diff -Nru passlib-1.5.3/CHANGES passlib-1.6.1/CHANGES --- passlib-1.5.3/CHANGES 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/CHANGES 2012-08-02 19:26:25.000000000 +0000 @@ -4,45 +4,362 @@ Release History =============== -**1.5.3** (2011-10-08) +**1.6.1** (2012-08-02) ====================== - Bugfix release -- fixes BCrypt padding/verification issue + Minor bugfix release + + * *bugfix*: Various :class:`~passlib.context.CryptContext` methods + would incorrectly raise :exc:`TypeError` if passed a :class:`!unicode` + user category under Python 2. For consistency, + :class:`!unicode` user category values are now encoded to ``utf-8`` :class:`bytes` under Python 2. + + * *bugfix*: Reworked internals of the :class:`CryptContext` config compiler + to fix a couple of border cases (:issue:`39`): + + - It will now throw a :exc:`ValueError` + if the :ref:`default ` scheme is marked as + :ref:`deprecated `. + - If no default scheme is specified, it will use the first + *non-deprecated* scheme. + - Finally, it will now throw a :exc:`ValueError` if all schemes + are marked as deprecated. + + * *bugfix*: FreeBSD 8.3 added native support for :class:`~passlib.hash.sha256_crypt` -- + updated Passlib's unittests and documentation accordingly (:issue:`35`). + + * *bugfix:* Fixed bug which caused passlib.apache unittest to fail + if mtime resolution >= 1 second (:issue:`35`). + + * Various bugfixes for Python 3.3 compatibility. + + * Various documentation updates and corrections. + +**1.6** (2012-05-01) +==================== + +.. _whats-new: + +Overview +-------- + + Welcome to Passlib 1.6. + + The main goal of this release was to clean up the codebase, tighten input + validation, and simplify the publically exposed interfaces. This release also + brings a number of other improvements: 10 or so new hash algorithms, + additional security precautions for the existing algorithms, + a number of speed improvements, and updated documentation. + +Deprecated APIs +............... + In order to improve the publically exposed interface, + some of the more cumbersome and less-used functions in Passlib + have been deprecated / renamed. This should not affect 99% of applications. + That said, all the deprecated interfaces are still present, and will continue + to be supported for at least one more major release. + To help with migration, all deprecated functions should issue an informative + :exc:`DeprecationWarning` when they are invoked, detailing their suggested replacement. + The following interfaces have changed: + + * The semi-internal :class:`!CryptPolicy` class has been deprecated + in it's entirety. All functionality has been rolled into the + parent :class:`!CryptContext` class (see :ref:`below ` for more). + + * The interface of the :mod:`passlib.apache` classes has been improved: + some confusing methods and options have been renamed, some new + constructors and other functions have been added. + + * The (undocumented) :mod:`!passlib.win32` module has been deprecated, + all of it's functionality is now offered through the + :doc:`lmhash ` and :doc:`nthash ` + algorithms. + +New Hashes +---------- + The release adds support for a number of hash algorithms: + + :doc:`cisco_pix `, :doc:`cisco_type7 ` + Two hash formats frequently found on various + Cisco devices *(for Cisco Type 5 hashes, see* + :doc:`md5_crypt ` *).* + + :ref:`django_pbkdf2_sha256 `, :ref:`django_pbkdf2_sha1 `, :ref:`django_bcrypt ` + All three of the new hash schemes introduced in Django 1.4. + + :doc:`lmhash `, :doc:`nthash ` + Microsoft's legacy "Lan Manager" hash, and the replacement + NT password hash. *(the old* ``nthash`` *algorithm in Passlib 1.5 has been renamed to* + :class:`~passlib.hash.bsd_nthash` *, to reflect it's lineage)*. + + :doc:`msdcc `, :doc:`msdcc2 ` + Microsoft Windows' Domain Cached Credentials, versions 1 and 2. + These algorithms also go by the names "DCC", "MSCache", and "MSCash". + + :doc:`mssql2000 `, :doc:`mssql2005 ` + Hash algorithms used by MS SQL Server 2000 and later. + + :doc:`scram ` + A hash format added specifically for storing the complex digest + information needed to authenticate a user via the SCRAM protocol + (:rfc:`5802`). It can also be used in the same way as any other + password hash in Passlib. + +Existing Hashes +--------------- + Additionally, the following new features have been added to the existing hashes: + + .. _password-size-limit: + + *Password Size Limit* + All hashes in Passlib will now throw :exc:`~passlib.exc.PasswordSizeError` + if handed a password that's larger than 4096 characters. + + This limit should be larger than any reasonable password size, + and prevents various things including DOS abuses, and exploitation + of OSes with a buggy :func:`!crypt` implementation. + See :exc:`~passlib.exc.PasswordSizeError` for how to change this limit. + + .. _consteq-issue: + + *Constant Time Comparison* + All hash comparisons in Passlib now use the "constant time" [#consteq]_ + comparison function :func:`~passlib.utils.consteq`, instead + of ``==``. + + This change is motivated a well-known `hmac timing attack `_ + which exploits short-circuit string comparisons. + While this attack is not currently feasible against + most password hashes, some of the weaker unsalted + hashes supported by Passlib may be vulnerable; and this + change has been made preventatively to all of them. + + .. [#consteq] "constant time" is a misnomer, it actually takes ``THETA(len(righthand_value))`` time. + + .. _strict-parameters: + + *Strict Parameters* + Previous releases of Passlib would silently correct any invalid values + (such as ``rounds`` parameters that were out of range). This is was deemed + undesirable, as it leaves developers unaware they are requesting + an incorrect (and potentially insecure) value. + + Starting with this release, providing invalid values to + :meth:`PasswordHash.encrypt ` + will result in a :exc:`ValueError`. However, most hashes now accept + an optional ``relaxed=True`` keyword, which causes Passlib + to try and correct invalid values, and if successful, + issue a :exc:`~passlib.exc.PasslibHashWarning` instead. + These warnings can then be filtered if desired. + + :doc:`bcrypt ` + The BCrypt hash now supports the `crypt_blowfish `_ project's + ``$2y$`` hash prefix. + + On an unrelated note, Passlib now offers an (experimental) pure-python + implementation of BCrypt. Unfortunately, it's still *WAY* too slow to be + suitable for production use; and is disabled by default. + If you really need it, see the BCrypt :ref:`documentation ` + for how to enable it. + + :doc:`bsdi_crypt ` + BSDi-Crypt will now issue a :exc:`~passlib.exc.PasslibSecurityWarning` + if an application requests an even number of rounds, due to + a known weakness in DES. Existing hashes with an even number of rounds will + now be flagged by :meth:`CryptContext.needs_update() `. + + :doc:`ldap_salted_{digest} ` + The LDAP salted digests now support salts of any size + from 4-16 bytes, though they still default to 4 (:issue:`30`). + + :doc:`md5_crypt `, :doc:`sha256_crypt `, :doc:`sha512_crypt ` + The builtin implementation of these hashes has been + sped up by about 25%, using an additional pre-computation step. + + :doc:`unix_disabled ` + The :class:`!unix_fallback` handler has been deprecated, + and will be removed in Passlib 1.8. + Applications should use the stricter-but-equivalent + :class:`!unix_disabled` handler instead. + + This most likely only affects internal Passlib code. + +.. _crypt-policy-deprecated: + +CryptContext +------------ + + .. currentmodule:: passlib.context + + The :ref:`CryptContext ` class has had a thorough + internal overhaul. While the primary interface has not changed + at all, the internals are much stricter about input validation, + common methods have shorter code-paths, and the + construction and introspection of :class:`!CryptContext` objects + has been greatly simplified. + Changes include: + + * All new (and hopefully clearer) :ref:`tutorial ` + and :ref:`reference ` documentation. + + * The :class:`CryptPolicy` class and the :attr:`!CryptContext.policy` attribute have been deprecated. + + This was a semi-internal class, which most applications + were not involved with at all, but to be conservative about + breaking things, the existing CryptPolicy interface + will remain in-place and supported until Passlib 1.8. + + All of the functionality of this class has been rolled into + :class:`!CryptContext` itself, so there's one less class to remember. + Many of the methods provided by :class:`!CryptPolicy` are now + :class:`!CryptContext` methods, most with the same name and call syntax. + Information on migrating existing code can be found in + the deprecation warnings issued by the class itself, + and in the :class:`CryptPolicy` documentation. + + * Two new class constructors have been added (:meth:`CryptContext.from_path` + and :meth:`CryptContext.from_string`) to aid in loading CryptContext objects + directly from a configuration file. + + * The :ref:`deprecated ` keyword + can now be set to the special string ``"auto"``; which will + automatically deprecate all schemes except for the default one. + + * The :ref:`min_verify_time ` keyword + has been deprecated, will be ignored in release 1.7, and will be removed in release 1.8. + It was never very useful, and now complicates the internal code needlessly. + + * All string parsing now uses stdlib's :class:`!SafeConfigParser`. + + Previous releases used the original :class:`!ConfigParser` interpolation; + which was deprecated in Passlib 1.5, and has now been removed. + This should only affect strings which contained raw ``%`` characters, + they will now need to be escaped via ``%%``. + +Other Modules +------------- + + * The api for the :mod:`passlib.apache` module has been updated + to add more flexibility, and to fix some ambiguous method + and keyword names. The old interface is still supported, but deprecated, + and will be removed in Passlib 1.8. + + * Added the :data:`~passlib.apps.django14_context` preset to the + the :mod:`!passlib.apps` module. this preconfigured CryptContext + object should support all the hashes found in a typical Django 1.4 + deployment. + + * **new**: Added :mod:`passlib.ext.django`, a Django plugin which can be used to + override Django's password hashing framework with a custom Passlib + policy (an undocumented beta version of this was present in the 1.5 release). + + * **new**: The :func:`passlib.utils.saslprep` function may be useful + for applications which need to normalize the unicode representation + of passwords before they are hashed. + +Bugfixes +-------- + * Handle platform-specific error strings that may be returned by the + :func:`!crypt` methods of some OSes. + + * Fixed rare ``'NoneType' object has no attribute 'decode'`` + error that sometimes occurred on platforms with a deviant implementation + of :func:`!crypt`. + +Internal Changes +---------------- + *The following changes should not affect most end users, + and have been documented just to keep track of them:* + + .. currentmodule:: passlib.utils.handlers + + * Passlib is now source-compatible with Python 2.5+ and Python 3.x. + It no longer requires the use of the :command:`2to3` command + to translate it for Python 3. + + * The unittest suite has been rewritten. It handles a number of additional + border cases, enforcing uniform behavior across all hashes, and + even features the addition of some simplistic fuzz testing. + It will take a bit longer to run though. While not perfect, + statement coverage is at about 95%. + Additionally, the hash test suite has been enhanced with many more + test vectors across the board, including 8-bit test vectors. + + * The internal framework used to construct the hash classes (:mod:`passlib.utils.handlers`) + was rewritten drastically. The new version provides stricter input checking, + reduction in boilerplate code. *These changes should not affect any + publically exposed routines*. + + - :class:`~passlib.utils.handlers.GenericHandler`'s + ``strict`` keyword was removed, ``strict=True`` is now + the class's default behavior: all values must be specified, + and be within the correct bounds. The new keywords + ``use_defaults`` and ``relaxed`` can be used to disable + these two requirements. + + - Most of the private methods of :class:`~passlib.utils.handlers.GenericHandler` + were renamed to begin with an underscore, to clarify their status; + and turned into instance methods, to simplify the internals. + (for example, :samp:`norm_salt` was renamed to :samp:`_norm_salt`). + + - :class:`~passlib.utils.handlers.StaticHandler` now derives from + :class:`!GenericHandler`, and requires ``_calc_checksum()`` be + implemented instead of ``encrypt()``. The old style is supported + but deprecated, and support will be removed in Passlib 1.8. + + - Calls to :meth:`HasManyBackends.set_backend` + should now use the string ``"any"`` instead of the value ``None``. + ``None`` was deprecated in release 1.5, and is no longer supported. + + .. currentmodule:: passlib.utils + + * :mod:`!passlib.utils.h64` has been replaced by an instance of the + new :class:`~passlib.utils.Base64Engine` class. This instance is + imported under the same name, and has (mostly) the same interface; + but should be faster, more flexible, and better unit-tested. + + * deprecated some unused support functions within :mod:`!passlib.utils`, + they will be removed in release 1.7. + +.. _bcrypt-padding-issue: - .. _bcrypt-padding-issue: +**1.5.3** (2011-10-08) +====================== + + Bugfix release -- fixes BCrypt padding/verification issue (:issue:`25`) This release fixes a single issue with Passlib's BCrypt support: Many BCrypt hashes generated by Passlib (<= 1.5.2) will not successfully verify under some of the other BCrypt implementations, such as OpenBSD's ``/etc/master.passwd``. - *In detail:* + *In detail:* BCrypt hashes contain 4 "padding" bits in the encoded salt, and Passlib (<= 1.5.2) generated salts in a manner which frequently set some of the padding bits to 1. While Passlib ignores these bits, many BCrypt - implementations perform password verification in a way will reject - *all* passwords, if any of the padding bits are set. Thus Passlib's - BCrypt salt generation needed to be corrected to ensure compatibility, - and a route provided to fix existing hashes already out in the wild - [issue 25]. - + implementations perform password verification in a way which rejects + *all* passwords if any of the padding bits are set. Thus Passlib's + BCrypt salt generation needed to be fixed to ensure compatibility, + and a route provided to correct existing hashes already out in the wild + :issue:`25`. + *Changes in this release:* - + .. currentmodule:: passlib.context - + * BCrypt hashes generated by Passlib now have all padding bits cleared. - + * Passlib will continue to accept BCrypt hashes that have padding bits set, but when it encounters them, it will issue a :exc:`UserWarning` recommending that the hash should be fixed (see below). - + * Applications which use :meth:`CryptContext.verify_and_update` will have any such hashes automatically re-encoded the next time the user logs in. *To fix existing hashes:* - + If you have BCrypt hashes which might have their padding bits set, you can import :class:`!passlib.hash.bcrypt`, and call ``clean_hash = bcrypt.normhash(hash)``. @@ -59,8 +376,8 @@ .. currentmodule:: passlib.hash * *bugfix:* :class:`django_des_crypt` now accepts all - :mod:`Hash64 ` characters in it's salts; - previously it accepted only lower-case hexidecimal characters [issue 22]. + :data:`hash64 ` characters in it's salts; + previously it accepted only lower-case hexidecimal characters (:issue:`22`). * Additional unittests added for all standard :doc:`Django hashes `. @@ -84,16 +401,16 @@ Minor bugfix release -- now compatible with Google App Engine. * *bugfix:* make ``passlib.hash.__loader__`` attribute writable - - needed by Google App Engine (GAE) [issue 19]. + needed by Google App Engine (GAE) :issue:`19`. * *bugfix:* provide fallback for loading ``passlib/default.cfg`` - if :mod:`pkg_resources` is not present, such as for GAE [issue 19]. + if :mod:`pkg_resources` is not present, such as for GAE :issue:`19`. * *bugfix:* fixed error thrown by CryptContext.verify - when issuing min_verify_time warning [issue 17]. + when issuing min_verify_time warning :issue:`17`. * removed min_verify_time setting from custom_app_context, - min_verify_time is too host & load dependant to be hardcoded [issue 17]. + min_verify_time is too host & load dependant to be hardcoded :issue:`17`. * under GAE, disable all unittests which require writing to filesystem. @@ -104,141 +421,148 @@ **1.5** (2011-07-11) ==================== - *"20% more unicode than the leading breakfast cereal"* - - The main new feature in this release is that - Passlib now supports Python 3 (via the 2to3 tool). - Everything has been recoded to have better separation - between unicode and bytes, and to use unicode internally - where possible. - When run under Python 2, Passlib 1.5 attempts - to provide the same behavior as Passlib 1.4; - but when run under Python 3, most functions - will return unicode instead of ascii bytes. - - Besides this major change, there have - been some other additions: +*"20% more unicode than the leading breakfast cereal"* - Hashes - - * added support for Cryptacular's PBKDF2 format. - * added support for the FSHP family of hashes. - * added support for using BCryptor as BCrypt backend. - * added support for all of Django's hash formats. +The main new feature in this release is that +Passlib now supports Python 3 (via the 2to3 tool). +Everything has been recoded to have better separation +between unicode and bytes, and to use unicode internally +where possible. +When run under Python 2, Passlib 1.5 attempts +to provide the same behavior as Passlib 1.4; +but when run under Python 3, most functions +will return unicode instead of ascii bytes. + +Besides this major change, there have +been some other additions: + +Hashes +------ + + * added support for Cryptacular's PBKDF2 format. + * added support for the FSHP family of hashes. + * added support for using BCryptor as BCrypt backend. + * added support for all of Django's hash formats. - CryptContext +CryptContext +------------ - .. currentmodule:: passlib.context + .. currentmodule:: passlib.context - * interpolation deprecation: + * interpolation deprecation: - :meth:`CryptPolicy.from_path` and :meth:`CryptPolicy.from_string` - now use :class:`!SafeConfigParser` instead of :class:`!ConfigParser`. - This may cause some existing config files containing unescaped ``%`` - to result in errors; Passlib 1.5 will demote these to warnings, - but any extant config files should be updated, - as the errors will be fatal in Passlib 1.6. - - * added encoding keyword to :class:`!CryptPolicy`'s - :meth:`!.from_path()`, :meth:`!.from_string`, - and :meth:`!.to_string` methods. - - * both classes in :mod:`passlib.apache` - now support specifying an encoding for the username/realm. - - Documentation - - * Password Hash API expanded to include explicit - :ref:`unicode vs bytes policy `. - * Added quickstart guide to documentation. - * Various minor improvements. - - Internals - - * Added more handler utility functions to reduce code duplication. - * Expanded kdf helpers in :mod:`passlib.utils.pbkdf2`. - * Removed deprecated parts of :mod:`passlib.utils.handlers`. - * Various minor changes to - :class:`passlib.utils.handlers.HasManyBackends`; - main change is that multi-backend handlers now raise - :exc:`~passlib.utils.MissingBackendError` - if no backends are available. - - Other - - * Builtin tests now use :mod:`!unittest2` if available. - * Setup script no longer requires distribute or setuptools. - * added (undocumented, experimental) Django app - for overriding Django's default hash format, - see ``docs/lib/passlib.ext.django.rst`` for more. + :meth:`CryptPolicy.from_path` and :meth:`CryptPolicy.from_string` + now use :class:`!SafeConfigParser` instead of :class:`!ConfigParser`. + This may cause some existing config files containing unescaped ``%`` + to result in errors; Passlib 1.5 will demote these to warnings, + but any extant config files should be updated, + as the errors will be fatal in Passlib 1.6. + + * added encoding keyword to :class:`!CryptPolicy`'s + :meth:`!.from_path()`, :meth:`!.from_string`, + and :meth:`!.to_string` methods. + + * both classes in :mod:`passlib.apache` + now support specifying an encoding for the username/realm. + +Documentation +------------- + + * Password Hash API expanded to include explicit + :ref:`unicode vs bytes policy `. + * Added quickstart guide to documentation. + * Various minor improvements. + +Internal Changes +---------------- + + * Added more handler utility functions to reduce code duplication. + * Expanded kdf helpers in :mod:`passlib.utils.pbkdf2`. + * Removed deprecated parts of :mod:`passlib.utils.handlers`. + * Various minor changes to + :class:`passlib.utils.handlers.HasManyBackends`; + main change is that multi-backend handlers now raise + :exc:`~passlib.exc.MissingBackendError` + if no backends are available. + + * Builtin tests now use :mod:`!unittest2` if available. + * Setup script no longer requires distribute or setuptools. + * added (undocumented, experimental) Django app + for overriding Django's default hash format, + see ``docs/lib/passlib.ext.django.rst`` for more. **1.4** (2011-05-04) ==================== - This release contains a large number of changes, both large and small. - It adds a number of PBKDF2-based schemes, better support - for LDAP-format hashes, improved documentation, - and faster load times. In detail... - - Hashes - - * added LDAP ``{CRYPT}`` support for all hashes - known to be supported by OS crypt() - * added 3 custom PBKDF2 schemes for general use, - as well as 3 LDAP-compatible versions. - * added support for Dwayne Litzenberger's PBKDF2 scheme. - * added support for Grub2's PBKDF2 hash scheme. - * added support for Atlassian's PBKDF2 password hash - * added support for all hashes used by the Roundup Issue Tracker - * bsdi_crypt, sha1_crypt now check for OS crypt() support - * ``salt_size`` keyword added to encrypt() method of all - the hashes which support variable-length salts. - * security fix: disabled unix_fallback's "wildcard password" support - unless explicitly enabled by user. - - CryptContext - - * host_context now dynamically detects which formats - OS crypt() supports, instead of guessing based on sys.platform. - * added predefined context for Roundup Issue Tracker database. - * added CryptContext.verify_and_update() convience method, - to make it easier to perform both operations at once. - * *bugfix:* fixed NameError in category+min_verify_time border case - * apps & hosts modules now use new - :class:`LazyCryptContext` wrapper class - - this should speed up initial import, - and reduce memory by not loading uneeded hashes. - - Documentation - - * greatly expanded documentation on how to use CryptContexts. - * roughly documented framework for writing & testing - custom password handlers. - * various minor improvements. - - Internals - - * added generate_password() convenience method - * refactored framework for building hash handlers, - using new mixin-based system. - * deprecated old handler framework - will remove in 1.5 - * deprecated list_to_bytes & bytes_to_list - not used, will remove in 1.5 - - Other - - * password hash api - as part of cleaning up optional attributes - specification, renamed a number of them to reduce ambiguity: - - - renamed *{xxx}_salt_chars* attributes -> *xxx_salt_size* - - renamed *salt_charset* -> *salt_chars* - - old attributes still present, but deprecated - will remove in 1.5 +This release contains a large number of changes, both large and small. +It adds a number of PBKDF2-based schemes, better support +for LDAP-format hashes, improved documentation, +and faster load times. In detail... + +Hashes +------ + + * added LDAP ``{CRYPT}`` support for all hashes + known to be supported by OS crypt() + * added 3 custom PBKDF2 schemes for general use, + as well as 3 LDAP-compatible versions. + * added support for Dwayne Litzenberger's PBKDF2 scheme. + * added support for Grub2's PBKDF2 hash scheme. + * added support for Atlassian's PBKDF2 password hash + * added support for all hashes used by the Roundup Issue Tracker + * bsdi_crypt, sha1_crypt now check for OS crypt() support + * ``salt_size`` keyword added to encrypt() method of all + the hashes which support variable-length salts. + * security fix: disabled unix_fallback's "wildcard password" support + unless explicitly enabled by user. + +CryptContext +------------ + + * host_context now dynamically detects which formats + OS crypt() supports, instead of guessing based on sys.platform. + * added predefined context for Roundup Issue Tracker database. + * added CryptContext.verify_and_update() convience method, + to make it easier to perform both operations at once. + * *bugfix:* fixed NameError in category+min_verify_time border case + * apps & hosts modules now use new + :class:`LazyCryptContext` wrapper class - + this should speed up initial import, + and reduce memory by not loading uneeded hashes. + +Documentation +------------- + + * greatly expanded documentation on how to use CryptContexts. + * roughly documented framework for writing & testing + custom password handlers. + * various minor improvements. + +Internals +--------- + + * added generate_password() convenience method + * refactored framework for building hash handlers, + using new mixin-based system. + * deprecated old handler framework - will remove in 1.5 + * deprecated list_to_bytes & bytes_to_list - not used, will remove in 1.5 + +Other +----- + + * password hash api - as part of cleaning up optional attributes + specification, renamed a number of them to reduce ambiguity: + + - renamed *{xxx}_salt_chars* attributes -> *xxx_salt_size* + - renamed *salt_charset* -> *salt_chars* + - old attributes still present, but deprecated - will remove in 1.5 - * password hash api - tightened specifications for salt & rounds parameters, - added support for hashes w/ no max salt size. + * password hash api - tightened specifications for salt & rounds parameters, + added support for hashes w/ no max salt size. - * improved password hash api conformance tests + * improved password hash api conformance tests - * PyPy compatibility + * PyPy compatibility **1.3.1** (2011-03-28) ====================== @@ -268,7 +592,7 @@ .. note:: - For this and all previous versions, PassLib did not exist independantly, + For this and all previous versions, Passlib did not exist independantly, but as a subpackage of *BPS*, a private & unreleased toolkit library. * many bugfixes @@ -280,7 +604,7 @@ ==================== * CryptContext & CryptHandler framework - * added support for: des-crypt, bcrypt (via pybcrypt), postgres, mysql + * added support for: des-crypt, bcrypt (via py-bcrypt), postgres, mysql * added unit tests **0.5** (2008-05-10) diff -Nru passlib-1.5.3/LICENSE passlib-1.6.1/LICENSE --- passlib-1.5.3/LICENSE 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/LICENSE 2012-05-03 16:36:58.000000000 +0000 @@ -10,7 +10,7 @@ and is released under the `BSD license `_:: Passlib - Copyright (c) 2008-2011 Assurance Technologies, LLC. + Copyright (c) 2008-2012 Assurance Technologies, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -42,7 +42,7 @@ Licenses for incorporated software ================================== -PassLib contains some code derived from the following sources: +Passlib contains some code derived from the following sources: MD5-Crypt --------- @@ -80,3 +80,24 @@ converted to python Jun 2009 by Eli Collins + +jBCrypt +------- +The source file ``passlib/utils/_blowfish/base.py`` contains code derived +from `jBcrypt 0.2 `_, a Java +implementation of the BCrypt password hash algorithm. It is available under +a BSD/ISC license:: + + Copyright (c) 2006 Damien Miller + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff -Nru passlib-1.5.3/MANIFEST.in passlib-1.6.1/MANIFEST.in --- passlib-1.5.3/MANIFEST.in 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/MANIFEST.in 2012-05-03 16:36:58.000000000 +0000 @@ -1,4 +1,3 @@ recursive-include docs * -include LICENSE README CHANGES passlib/*.cfg passlib/tests/*.cfg -prune docs/_build +include LICENSE README CHANGES passlib/*.cfg passlib/tests/*.cfg tox.ini setup.cfg prune *.komodoproject diff -Nru passlib-1.5.3/PKG-INFO passlib-1.6.1/PKG-INFO --- passlib-1.5.3/PKG-INFO 2011-10-08 04:58:50.000000000 +0000 +++ passlib-1.6.1/PKG-INFO 2012-08-02 19:28:07.000000000 +0000 @@ -1,27 +1,26 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: passlib -Version: 1.5.3 -Summary: comprehensive password hashing framework supporting over 20 schemes +Version: 1.6.1 +Summary: comprehensive password hashing framework supporting over 30 schemes Home-page: http://passlib.googlecode.com Author: Eli Collins Author-email: elic@assurancetechnologies.com License: BSD -Download-URL: http://passlib.googlecode.com/files/passlib-1.5.3.tar.gz -Description: Passlib is a password hashing library for Python 2 & 3, - which provides cross-platform implementations of over 20 - password hashing algorithms, as well as a framework for - managing existing password hashes. It's designed to be useful - for a wide range of tasks, from verifying a hash found in /etc/shadow, - to providing full-strength password hashing for multi-user application. +Download-URL: http://passlib.googlecode.com/files/passlib-1.6.1.tar.gz +Description: Passlib is a password hashing library for Python 2 & 3, which provides + cross-platform implementations of over 30 password hashing algorithms, as well + as a framework for managing existing password hashes. It's designed to be useful + for a wide range of tasks, from verifying a hash found in /etc/shadow, to + providing full-strength password hashing for multi-user application. * See the `online documentation `_ for details, installation instructions, and examples. - * See the `passlib homepage `_ + * See the `Passlib homepage `_ for the latest news, more information, and additional downloads. * See the `changelog `_ - for description of what's new in Passlib. + for a description of what's new in Passlib. All releases are signed with the gpg key `4CE1ED31 `_. @@ -36,6 +35,9 @@ Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: Jython +Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Libraries Classifier: Development Status :: 5 - Production/Stable diff -Nru passlib-1.5.3/README passlib-1.6.1/README --- passlib-1.5.3/README 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/README 2012-05-03 16:36:58.000000000 +0000 @@ -1,24 +1,23 @@ .. -*- restructuredtext -*- ========================== -The PassLib Python Library +The Passlib Python Library ========================== Welcome ======= -Passlib is a password hashing library for Python 2 & 3, -which provides cross-platform implementations of over 20 -password hashing algorithms, as well as a framework for -managing existing password hashes. It's designed to be useful -for a wide range of tasks, from verifying a hash found in /etc/shadow, -to providing full-strength password hashing for multi-user application. +Passlib is a password hashing library for Python 2 & 3, which provides +cross-platform implementations of over 30 password hashing algorithms, as well +as a framework for managing existing password hashes. It's designed to be useful +for a wide range of tasks, from verifying a hash found in /etc/shadow, to +providing full-strength password hashing for multi-user application. The latest documentation can be found online at ``_. Requirements ============ -* Python 2.5 - 2.7 or Python 3 -* PyBCrypt or BCryptor (optional; required only if bcrypt support is needed) +* Python 2.5 - 2.7 or Python 3.x +* py-bcrypt or bcryptor (optional; required only if bcrypt support is needed) * M2Crypto (optional; accelerates PBKDF2-based hashes) Installation diff -Nru passlib-1.5.3/debian/changelog passlib-1.6.1/debian/changelog --- passlib-1.5.3/debian/changelog 2012-02-06 15:33:15.000000000 +0000 +++ passlib-1.6.1/debian/changelog 2013-08-20 16:44:14.000000000 +0000 @@ -1,20 +1,26 @@ -passlib (1.5.3-0ubuntu1) precise; urgency=low +passlib (1.6.1-1chl1~quantal1) quantal; urgency=low + + * New upstream version. + + -- Chris Lea Tue, 20 Aug 2013 09:43:44 -0700 + +passlib (1.5.3-0ubuntu1) quantal; urgency=low * New upstream version. * Build for Python 3. -- Chuck Short Mon, 06 Feb 2012 10:23:46 -0500 -passlib (1.5.2-1ubuntu2) precise; urgency=low +passlib (1.5.2-1ubuntu2) quantal; urgency=low * Enable testsuite. * Fix lintian warnings. -- Chuck Short Mon, 06 Feb 2012 10:23:31 -0500 -passlib (1.5.2-1ubuntu1) precise; urgency=low +passlib (1.5.2-1ubuntu1) quantal; urgency=low - * Uploaded for precise. + * Uploaded for quantal. * Transition for dh_python2. -- Chuck Short Thu, 27 Oct 2011 08:58:28 -0400 diff -Nru passlib-1.5.3/docs/conf.py passlib-1.6.1/docs/conf.py --- passlib-1.5.3/docs/conf.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/docs/conf.py 2012-08-01 17:16:44.000000000 +0000 @@ -1,53 +1,71 @@ # -*- coding: utf-8 -*- -# -# PassLib documentation build configuration file, created by -# sphinx-quickstart on Mon Mar 2 14:12:06 2009. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +""" +Sphinx configuration file for Passlib documentation. +This file is execfile()d with the current directory set to its containing dir. +Note that not all possible configuration values are present in this +autogenerated file. All configuration values have a default; values that are +commented out serve to show the default. +""" +#============================================================================= +# environment setup +#============================================================================= import sys, os -options = os.environ.get("PASSLIB_DOCS", "") - -#make sure passlib in sys.path -doc_root = os.path.abspath(os.path.join(__file__,os.path.pardir)) -source_root = os.path.abspath(os.path.join(doc_root,os.path.pardir)) +# make sure passlib in sys.path +doc_root = os.path.abspath(os.path.dirname(__file__)) +source_root = os.path.dirname(doc_root) sys.path.insert(0, source_root) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -#building the docs requires the Cloud sphinx theme & extensions -# https://bitbucket.org/ecollins/cloud_sptheme -#which contains some sphinx extensions used by passlib -import cloud_sptheme - -#hack to make autodoc generate documentation from the correct class... -from passlib.utils import md4 -md4.md4 = md4._builtin_md4 +##sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +#============================================================================= +# imports +#============================================================================= + +# load build option flags +options = os.environ.get("PASSLIB_DOCS", "").split(",") + +# building the docs requires the Cloud Sphinx theme & extensions (>= v1.4), +# which contains some sphinx extensions used by Passlib. +# (https://bitbucket.org/ecollins/cloud_sptheme) +import cloud_sptheme as csp + +# hack to make autodoc generate documentation from the correct class... +import passlib.utils.md4 as md4_mod +md4_mod.md4 = md4_mod._builtin_md4 + +#============================================================================= +# General configuration +#============================================================================= # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.0' +needs_sphinx = '1.1' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + # standard sphinx extensions 'sphinx.ext.autodoc', 'sphinx.ext.todo', - 'cloud_sptheme.ext.autodoc_sections', #add autdoc support for ReST sections in class/function docstrings - 'cloud_sptheme.ext.index_styling', #adds extra ids & classes to genindex html, for additional styling - 'cloud_sptheme.ext.relbar_toc', #inserts toc into right hand nav bar (ala old style python docs) + # add autdoc support for ReST sections in class/function docstrings + 'cloud_sptheme.ext.autodoc_sections', + + # adds extra ids & classes to genindex html, for additional styling + 'cloud_sptheme.ext.index_styling', + + # inserts toc into right hand nav bar (ala old style python docs) + 'cloud_sptheme.ext.relbar_toc', + + # replace sphinx :samp: role handler with one that allows escaped {} chars + 'cloud_sptheme.ext.escaped_samp_literals', + + # add "issue" role + 'cloud_sptheme.ext.issue_tracker', ] # Add any paths that contain templates here, relative to this directory. @@ -61,86 +79,99 @@ # The master toctree document. master_doc = 'contents' + +# The frontpage document. index_doc = 'index' # General information about the project. -project = u'PassLib' -copyright = u'2008-2011, Assurance Technologies, LLC' +project = 'Passlib' +copyright = '2008-2012, Assurance Technologies, LLC' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# -# version: The short X.Y version. # release: The full version, including alpha/beta/rc tags. +# version: The short X.Y version. from passlib import __version__ as release -version = cloud_sptheme.get_version(release) +version = csp.get_version(release) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +##language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +##today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +##today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ - #disabling documentation of this until module is more mature. - "lib/passlib.ext.django.rst" + # disabling documentation of this until module is more mature. + "lib/passlib.utils.compat.rst", + + # may remove this in future release + "lib/passlib.utils.md4.rst", ] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +##default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +##add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +##show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -modindex_common_prefix = [ "passlib." ] - -# -- Options for all output --------------------------------------------------- -todo_include_todos = "hide-todos" not in options -keep_warnings = "hide-warnings" not in options +modindex_common_prefix = ["passlib."] -# -- Options for HTML output --------------------------------------------------- +#============================================================================= +# Options for all output +#============================================================================= +todo_include_todos = True +keep_warnings = True +issue_tracker_url = "gc:passlib" + +#============================================================================= +# Options for HTML output +#============================================================================= # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'cloud' +html_theme = 'redcloud' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -if html_theme == 'cloud': - html_theme_options = { "roottarget": index_doc } -else: - html_theme_options = {} +html_theme_options = {} +if csp.is_cloud_theme(html_theme): + html_theme_options.update(roottarget=index_doc) + if 'for-pypi' in options: + html_theme_options.update( + googleanalytics_id = 'UA-22302196-2', + googleanalytics_path = '/passlib/', + ) # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [cloud_sptheme.get_theme_dir()] +html_theme_path = [csp.get_theme_dir()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = project + " v" + release + " Documentation" +html_title = "%s v%s Documentation" % (project, release) # A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = project + " Documentation" +html_short_title = "%s %s Documentation" % (project, version) # The name of an image file (relative to this directory) to place at the top # of the sidebar. @@ -165,86 +196,92 @@ html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +##html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +##html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +##html_domain_indices = True # If false, no index is generated. -#html_use_index = True +##html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +##html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +##html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +##html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +##html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +##html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +##html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = project + 'Doc' - -# -- Options for LaTeX output -------------------------------------------------- +#============================================================================= +# Options for LaTeX output +#============================================================================= # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +##latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +##latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - (index_doc, project + '.tex', project + u' Documentation', - u'Assurance Technologies, LLC', 'manual'), + (index_doc, project + '.tex', project + ' Documentation', + 'Assurance Technologies, LLC', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +##latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +##latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +##latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +##latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +##latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +##latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True - +##latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +#============================================================================= +# Options for manual page output +#============================================================================= # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (index_doc, project, project + u' Documentation', - [u'Assurance Technologies, LLC'], 1) + (index_doc, project, project + ' Documentation', + ['Assurance Technologies, LLC'], 1) ] + +#============================================================================= +# EOF +#============================================================================= diff -Nru passlib-1.5.3/docs/conf.py.orig passlib-1.6.1/docs/conf.py.orig --- passlib-1.5.3/docs/conf.py.orig 2011-10-08 04:42:00.000000000 +0000 +++ passlib-1.6.1/docs/conf.py.orig 1970-01-01 00:00:00.000000000 +0000 @@ -1,264 +0,0 @@ -# -*- coding: utf-8 -*- -# -# PassLib documentation build configuration file, created by -# sphinx-quickstart on Mon Mar 2 14:12:06 2009. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -options = os.environ.get("PASSLIB_DOCS", "").split(",") - -#make sure passlib in sys.path -doc_root = os.path.abspath(os.path.join(__file__,os.path.pardir)) -source_root = os.path.abspath(os.path.join(doc_root,os.path.pardir)) -sys.path.insert(0, source_root) - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -#building the docs requires the Cloud sphinx theme & extensions -# https://bitbucket.org/ecollins/cloud_sptheme -#which contains some sphinx extensions used by passlib -import cloud_sptheme - -#hack to make autodoc generate documentation from the correct class... -from passlib.utils import md4 -md4.md4 = md4._builtin_md4 - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - - 'cloud_sptheme.ext.autodoc_sections', #add autdoc support for ReST sections in class/function docstrings - 'cloud_sptheme.ext.index_styling', #adds extra ids & classes to genindex html, for additional styling - 'cloud_sptheme.ext.relbar_toc', #inserts toc into right hand nav bar (ala old style python docs) -## 'cloud_sptheme.ext.issue_tracker', - ] - -# only used for packages.python.org deployment -try: - import sphinxcontrib.googleanalytics -except ImportError: - pass -else: - extensions.append("sphinxcontrib.googleanalytics") - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'contents' -index_doc = 'index' - -# General information about the project. -project = u'PassLib' -copyright = u'2008-2011, Assurance Technologies, LLC' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# - -# version: The short X.Y version. -# release: The full version, including alpha/beta/rc tags. -from passlib import __version__ as release -version = cloud_sptheme.get_version(release) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [ - #disabling documentation of this until module is more mature. - "lib/passlib.ext.django.rst" -] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -modindex_common_prefix = [ "passlib." ] - -# -- Options for all output --------------------------------------------------- -todo_include_todos = "hide-todos" not in options -keep_warnings = "hide-warnings" not in options - -issue_tracker_template = "gc:passlib" - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'cloud' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -if html_theme == 'cloud': - html_theme_options = { "roottarget": index_doc } - gaid = os.environ.get("PASSLIB_GA_ID") - if gaid: - html_theme_options['googleanalytics_id'] = gaid -else: - html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [cloud_sptheme.get_theme_dir()] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = project + " v" + release + " Documentation" - -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = project + " Documentation" - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = os.path.join("_static", "masthead.png") - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = "logo.ico" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = project + 'Doc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - (index_doc, project + '.tex', project + u' Documentation', - u'Assurance Technologies, LLC', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (index_doc, project, project + u' Documentation', - [u'Assurance Technologies, LLC'], 1) -] diff -Nru passlib-1.5.3/docs/contents.rst passlib-1.6.1/docs/contents.rst --- passlib-1.5.3/docs/contents.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/contents.rst 2012-08-01 19:19:51.000000000 +0000 @@ -3,25 +3,28 @@ ================= .. toctree:: + :maxdepth: 4 Front Page install - new_app_quickstart - overview + new_app_quickstart - password_hash_api lib/passlib.hash + lib/passlib.context-tutorial lib/passlib.context lib/passlib.apps - lib/passlib.apache lib/passlib.hosts + lib/passlib.apache + lib/passlib.ext.django + + lib/passlib.exc lib/passlib.registry lib/passlib.utils - + modular_crypt_format history @@ -29,8 +32,3 @@ * :ref:`General Index ` * :ref:`Module List ` - -.. - unlisted: - - lib/passlib.ext.django diff -Nru passlib-1.5.3/docs/index.rst passlib-1.6.1/docs/index.rst --- passlib-1.5.3/docs/index.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/index.rst 2012-08-01 20:24:07.000000000 +0000 @@ -1,77 +1,119 @@ ========================================== -PassLib |release| documentation +Passlib |release| documentation ========================================== Welcome ======= -Passlib is a password hashing library for Python 2 & 3, -which provides cross-platform implementations of over 20 password hashing algorithms, -as well as a framework for managing existing password hashes. -It's designed to be useful for a large range of tasks, including: - -* quick-start password hashing for new python applications ~ - :doc:`quickstart guide ` - -* constructing a configurable hashing policy - to match the needs of any python application ~ - :data:`passlib.context` - -* reading & writing Apache htpasswd / htdigest files ~ - :mod:`passlib.apache` - -* creating & verifying hashes used by MySQL, PostgreSQL, OpenLDAP, - and other applications ~ - :mod:`passlib.apps` - -* creating & verifying hashes found in Unix "shadow" files ~ - :data:`passlib.hosts` - -See the library overview for more details and usage examples. - -Quick Links -=========== - -.. raw:: html - - - - - - -
- - - - - - - -
+Passlib is a password hashing library for Python 2 & 3, which provides +cross-platform implementations of over 30 password hashing algorithms, as well +as a framework for managing existing password hashes. It's designed to be useful +for a wide range of tasks, from verifying a hash found in /etc/shadow, to +providing full-strength password hashing for multi-user application. + +As a quick sample, the following code hashes and then verifies a password +using the :doc:`SHA256-Crypt
` algorithm:: + + >>> # import the hash algorithm + >>> from passlib.hash import sha256_crypt + + >>> # generate new salt, and hash a password + >>> hash = sha256_crypt.encrypt("toomanysecrets") + >>> hash + '$5$rounds=80000$zvpXD3gCkrt7tw.1$QqeTSolNHEfgryc5oMgiq1o8qCEAcmye3FoMSuvgToC' + + >>> # verifying the password + >>> sha256_crypt.verify("toomanysecrets", hash) + True + >>> sha256_crypt.verify("joshua", hash) + False + +Content Summary +=============== + +.. rst-class:: floater + +.. seealso:: :ref:`What's new in Passlib 1.6 ` + +Introductory Materials +---------------------- + + :doc:`install` + requirements & installation instructions + + :doc:`overview` + describes how Passlib is laid out + + :doc:`New Application Quickstart ` + choosing a password hash for new applications + +---- + +Password Hashing Algorithms +--------------------------- + :mod:`passlib.hash` + all the password hashes supported by Passlib -- + - :doc:`Overview ` + - :ref:`mcf-hashes` + - :ref:`ldap-hashes` + - :ref:`database-hashes` + - :ref:`windows-hashes` + - :ref:`other-hashes` + + :doc:`PasswordHash interface ` + examples & documentation of the common hash interface + used by all the hash algorithms in Passlib. + +CryptContext Objects +-------------------- + :mod:`passlib.context` + provides the :class:`!CryptContext` class, a flexible container + for managing and migrating between multiple hash algorithms. + + :mod:`passlib.apps` + predefined CryptContext objects for managing the hashes used by + MySQL, PostgreSQL, OpenLDAP, and others applications. + + :mod:`passlib.hosts` + predefined CryptContext objects for managing the hashes + found in Linux & BSD "shadow" files. + +Application Helpers +------------------- + :mod:`passlib.apache` + classes for manipulating Apache's ``htpasswd`` and ``htdigest`` files. + + :mod:`passlib.ext.django` + Django plugin which monkeypatches support for (almost) any hash in Passlib. + +.. + Support Modules + --------------- + :mod:`passlib.exc` + + custom warnings and exceptions used by Passlib + :mod:`passlib.registry` + :mod:`passlib.utils` + +---- + +Other Documents +--------------- + :doc:`modular_crypt_format` + reference listing "modular crypt format" support across Unix systems. + + :doc:`Changelog ` + Passlib's release history Online Resources ================ - .. rst-class:: html-plain-table - ================ =================================================== - **Homepage**: ``_ - **Online Docs**: ``_ - **Discussion**: ``_ + Homepage: ``_ + Online Docs: ``_ + Discussion: ``_ ---------------- --------------------------------------------------- ---------------- --------------------------------------------------- - **PyPI**: ``_ - **Downloads**: ``_ - **Source**: ``_ + PyPI: ``_ + Downloads: ``_ + Source: ``_ ================ =================================================== diff -Nru passlib-1.5.3/docs/install.rst passlib-1.6.1/docs/install.rst --- passlib-1.5.3/docs/install.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/install.rst 2012-08-02 14:57:41.000000000 +0000 @@ -2,6 +2,8 @@ Installation ============ +.. index:: Google App Engine; compatibility + Supported Platforms =================== Passlib requires Python 2 (>= 2.5) or Python 3. @@ -17,11 +19,11 @@ for almost all OS-dependant features. Google App Engine is supported as well. -.. _optional-libraries: +.. _optional-libraries: Optional Libraries ================== -* `py-bcrypt `_ or +* `py-bcrypt `_ or `bcryptor `_ If either of these packages are installed, they will be used to provide @@ -38,62 +40,57 @@ Installation Instructions ========================= -To download and install using :command:`easy_install`:: +To install from PyPi using :command:`pip`:: - easy_install passlib + pip install passlib -To download and install using :command:`pip`:: +To install from PyPi using :command:`easy_install`:: - pip install passlib + easy_install passlib -To install from a source directory using :command:`setup.py`:: +To install from the source using :command:`setup.py`:: python setup.py install -.. note:: +.. index:: + pair: environmental variable; PASSLIB_TEST_MODE - Passlib's source ships as Python 2 code, - and the setup script invokes the :command:`2to3` tool + a preprocessor - to translate the source to Python 3 code at install time. - Aside from this internal detail, - installation under Python 3 - should be identical to that of Python 2. +.. rst-class:: html-toggle Testing ======= -PassLib contains a comprehensive set of unittests providing nearly complete coverage. +Passlib contains a comprehensive set of unittests (about 38% of the total code), +which provide nearly complete coverage, and verification of the hash +algorithms using multiple external sources (if detected at runtime). All unit tests are contained within the :mod:`passlib.tests` subpackage, and are designed to be run using the `Nose `_ unit testing library. -Once PassLib and Nose have been installed, the tests may be run from the source directory:: +Once Passlib and Nose have been installed, the main suite of tests may be run from the source directory:: + + nosetests --tests passlib/tests - # to run the platform-relevant tests... - nosetests -v --tests passlib/tests +To run the full test suite, which includes internal cross-checks and mock-testing +of features not provided natively by the host OS:: - # to run all tests... - PASSLIB_TESTS="all" nosetests -v --tests passlib/tests + PASSLIB_TEST_MODE="full" nosetests --tests passlib/tests - # to run nose with the optional coverage plugin... - # (results will be in build/coverage) - PASSLIB_TESTS="all" nosetests -v --tests passlib/tests --with-coverage \ - --cover-package=passlib --cover-html --cover-html-dir build/coverage +Tests may also be run via ``setup.py test`` or the included ``tox.ini`` file. -(There will be a large proportion of skipped tests, this is normal). +.. rst-class:: html-toggle -Documentation -============= +Building the Documentation +========================== The latest copy of this documentation should always be available online at ``_. - If you wish to generate your own copy of the documentation, you will need to: -1. Install `Sphinx `_ (1.0 or better) -2. Install the `Cloud Sphinx Theme `_. -3. Download the PassLib source -4. From the PassLib source directory, run :samp:`python setup.py build_sphinx`. -5. Once Sphinx completes it's run, point a web browser to the file at :samp:`{$SOURCE}/build/sphinx/html/index.html` - to access the PassLib documentation in html format. +1. Install `Sphinx `_ (1.1 or newer) +2. Install the `Cloud Sphinx Theme `_ (1.4 or newer). +3. Download the Passlib source +4. From the Passlib source directory, run :samp:`python setup.py build_sphinx`. +5. Once Sphinx completes it's run, point a web browser to the file at :samp:`{SOURCE}/build/sphinx/html/index.html` + to access the Passlib documentation in html format. 6. Alternately, steps 4 & 5 can be replaced by running :samp:`python setup.py docdist`, - which will build a zip file of the documentation in :samp:`{$SOURCE}/dist`. + which will build a zip file of the documentation in :samp:`{SOURCE}/dist`. diff -Nru passlib-1.5.3/docs/lib/passlib.apache.rst passlib-1.6.1/docs/lib/passlib.apache.rst --- passlib-1.5.3/docs/lib/passlib.apache.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.apache.rst 2012-07-02 21:25:03.000000000 +0000 @@ -8,7 +8,15 @@ This module provides utilities for reading and writing Apache's htpasswd and htdigest files; though the use of two helper classes. -.. index:: apache; htpasswd +.. versionchanged:: 1.6 + The api for this module was updated to be more flexible, + and to have less ambiguous method names. + The old method and keyword names are deprecated, and + will be removed in Passlib 1.8. + No more backwards-incompatible changes are currently planned + for these classes. + +.. index:: Apache; htpasswd Htpasswd Files ============== @@ -17,43 +25,43 @@ >>> from passlib.apache import HtpasswdFile - >>> #when creating a new file, set to autoload=False, add entries, and save. - >>> ht = HtpasswdFile("test.htpasswd", autoload=False) - >>> ht.update("someuser", "really secret password") + >>> # when creating a new file, set to new=True, add entries, and save. + >>> ht = HtpasswdFile("test.htpasswd", new=True) + >>> ht.set_password("someuser", "really secret password") >>> ht.save() - >>> #loading an existing file to update a password + >>> # loading an existing file to update a password >>> ht = HtpasswdFile("test.htpasswd") - >>> ht.update("someuser", "new secret password") + >>> ht.set_password("someuser", "new secret password") >>> ht.save() - >>> #examining file, verifying user's password + >>> # examining file, verifying user's password >>> ht = HtpasswdFile("test.htpasswd") >>> ht.users() [ "someuser" ] - >>> ht.verify("someuser", "wrong password") + >>> ht.check_password("someuser", "wrong password") False - >>> ht.verify("someuser", "new secret password") + >>> ht.check_password("someuser", "new secret password") True - >>> #making in-memory changes and exporting to string + >>> # making in-memory changes and exporting to string >>> ht = HtpasswdFile() - >>> ht.update("someuser", "mypass") - >>> ht.update("someuser", "anotherpass") + >>> ht.set_password("someuser", "mypass") + >>> ht.set_password("someuser", "anotherpass") >>> print ht.to_string() someuser:$apr1$T4f7D9ly$EobZDROnHblCNPCtrgh5i/ anotheruser:$apr1$vBdPWvh1$GrhfbyGvN/7HalW5cS9XB1 -.. autoclass:: HtpasswdFile(path, default=None, autoload=True) +.. autoclass:: HtpasswdFile(path=None, new=False, autosave=False, ...) -.. index:: apache; htdigest +.. index:: Apache; htdigest Htdigest Files ============== The :class:`!HtdigestFile` class allows management of htdigest files in a similar fashion to :class:`HtpasswdFile`. -.. autoclass:: HtdigestFile(path, autoload=True) +.. autoclass:: HtdigestFile(path, default_realm=None, new=False, autosave=False, ...) .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.apps.rst passlib-1.6.1/docs/lib/passlib.apps.rst --- passlib-1.5.3/docs/lib/passlib.apps.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.apps.rst 2012-08-01 17:05:50.000000000 +0000 @@ -5,56 +5,90 @@ .. module:: passlib.apps :synopsis: encrypting & verifying passwords used in sql servers and other applications -This lists a number of :class:`!CryptContext` instances that are predefined -by PassLib for easily handling the multiple formats used by various applications. -(For details about how to use a :class:`!CryptContext` instance, -see :doc:`passlib.context-usage`). +.. _predefined-context-example: -.. _quickstart-custom-applications: +This module contains a number of preconfigured :ref:`CryptContext ` instances +that are provided by Passlib for easily handling the hash formats used by various applications. -Custom Applications -=================== -.. data:: custom_app_context +.. rst-class:: html-toggle - This :class:`!CryptContext` object is provided for new python applications - to quickly and easily add password hashing support. - It offers: +Usage Example +============= +The :class:`!CryptContext` class itself has a large number of features, +but to give an example of how to quickly use the instances in this module: - * Support for :class:`~passlib.hash.sha256_crypt` and :class:`~passlib.hash.sha512_crypt` - * Defaults to SHA256-Crypt under 32 bit systems; SHA512-Crypt under 64 bit systems. - * Comes pre-configured with strong rounds settings. +Each of the objects in this module can be imported directly:: - For applications which want to quickly add a password hash, - all they need to do is the following:: + >>> # as an example, this imports the custom_app_context object, + >>> # a helper to let new applications *quickly* add password hashing. + >>> from passlib.apps import custom_app_context - >>> #import the context under an app-specific name (so it can easily be replaced later) - >>> from passlib.apps import custom_app_context as pwd_context +Encrypting a password is simple (and salt generation is handled automatically):: - >>> #encrypting a password... - >>> hash = pwd_context.encrypt("somepass") + >>> hash = custom_app_context.encrypt("toomanysecrets") + >>> hash + '$5$rounds=84740$fYChCy.52EzebF51$9bnJrmTf2FESI93hgIBFF4qAfysQcKoB0veiI0ZeYU4' - >>> #verifying a password... - >>> ok = pwd_context.verify("somepass", hash) +Verifying a password against an existing hash is just as quick:: - >>> #[optional] encrypting a password for an admin account - uses stronger settings - >>> hash = pwd_context.encrypt("somepass", category="admin") + >>> custom_app_context.verify("toomanysocks", hash) + False + >>> custom_app_context.verify("toomanysecrets", hash) + True -.. seealso:: +.. seealso:: the :ref:`CryptContext Tutorial ` + and :ref:`CryptContext Reference ` + for more information about the CryptContext class. - The :doc:`/new_app_quickstart`. +.. index:: Django; crypt context -.. index:: django; crypt context +.. _django-contexts: Django ====== +The following objects provide pre-configured :class:`!CryptContext` instances +for handling `Django `_ +password hashes, as used by Django's ``django.contrib.auth`` module. +They recognize all the :doc:`builtin Django hashes ` +supported by the particular Django version. + +.. note:: + + These objects may not match the hashes in your database if a third-party + library has been used to patch Django to support alternate hash formats. + This includes the `django-bcrypt `_ + plugin, or Passlib's builtin :mod:`django extension `. + As well, Django 1.4 introduced a very configurable "hashers" framework, + and individual deployments may support additional hashes and/or + have other defaults. + +.. data:: django10_context + + The object replicates the password hashing policy for Django 1.0-1.3. + It supports all the Django 1.0 hashes, and defaults to + :class:`~passlib.hash.django_salted_sha1`. + + .. versionadded:: 1.6 + +.. data:: django14_context + + The object replicates the stock password hashing policy for Django 1.4. + It supports all the Django 1.0 & 1.4 hashes, and defaults to + :class:`~passlib.hash.django_pbkdf2_sha256`. It treats all + Django 1.0 hashes as deprecated. + + .. versionadded:: 1.6 + .. data:: django_context - This object provides a pre-configured :class:`!CryptContext` instance - for handling `Django `_ - password hashes, as used by Django's ``django.contrib.auth`` module. - It recognizes all the :doc:`builtin Django hashes `. - It defaults to using the :class:`~passlib.hash.django_salted_sha1` hash. - + This alias will always point to the latest preconfigured Django + context supported by Passlib, and as such should support + all historical hashes built into Django. + + .. versionchanged:: 1.6 + This previously was an alias for :data:`django10_context`, + and now points to :data:`django14_context`. + .. _ldap-contexts: LDAP @@ -73,7 +107,7 @@ >>> from passlib.apps import ldap_context >>> ldap_context = ldap_context.replace(default="ldap_salted_md5") - >>> #the new context object will now default to {SMD5}: + >>> # the new context object will now default to {SMD5}: >>> ldap_context.encrypt("password") '{SMD5}T9f89F591P3fFh1jz/YtW4aWD5s=' @@ -82,7 +116,7 @@ This object recognizes all the standard ldap schemes that :data:`!ldap_context` does, *except* for the ``{CRYPT}``-based schemes. -.. index:: mysql; crypt context +.. index:: MySQL; crypt context .. _mysql-contexts: @@ -107,7 +141,7 @@ This should be used only with MySQL version 3.2.3 - 4.0. -.. index:: drupal; crypt context, wordpress; crypt context, phpbb3; crypt context, phpass; crypt context +.. index:: Drupal; crypt context, Wordpress; crypt context, phpBB3; crypt context, PHPass; crypt context PHPass ====== @@ -134,7 +168,7 @@ This object supports phpbb3 password hashes, which use a variant of :class:`~passlib.hash.phpass`. -.. index:: postgres; crypt context +.. index:: Postgres; crypt context PostgreSQL ========== @@ -147,17 +181,23 @@ >>> from passlib.apps import postgres_context - >>> #encrypting a password... + >>> # encrypting a password... >>> postgres_context.encrypt("somepass", user="dbadmin") 'md578ed0f0ab2be0386645c1b74282917e7' - >>> #verifying a password... + >>> # verifying a password... >>> postgres_context.verify("somepass", 'md578ed0f0ab2be0386645c1b74282917e7', user="dbadmin") True >>> postgres_context.verify("wrongpass", 'md578ed0f0ab2be0386645c1b74282917e7', user="dbadmin") False -.. index:: roundup; crypt context + >>> # forgetting the user will result in an error: + >>> postgres_context.encrypt("somepass") + Traceback (most recent call last): + + TypeError: user must be unicode or bytes, not None + +.. index:: Roundup; crypt context Roundup ======= @@ -183,3 +223,25 @@ this is an alias for the latest version-specific roundup context supported by passlib, currently the :data:`!roundup15_context`. + +.. _quickstart-custom-applications: + +Custom Applications +=================== +.. data:: custom_app_context + + This :class:`!CryptContext` object is provided for new python applications + to quickly and easily add password hashing support. + It comes preconfigured with: + + * Support for :class:`~passlib.hash.sha256_crypt` and :class:`~passlib.hash.sha512_crypt` + * Defaults to SHA256-Crypt under 32 bit systems, SHA512-Crypt under 64 bit systems. + * Large number of ``rounds``, for increased time-cost to hedge against attacks. + + For applications which want to quickly add a password hash, + all they need to do is import and use this object, per the + :ref:`usage example ` at the top of this page. + + .. seealso:: + + The :doc:`/new_app_quickstart` for additional details. diff -Nru passlib-1.5.3/docs/lib/passlib.context-interface.rst passlib-1.6.1/docs/lib/passlib.context-interface.rst --- passlib-1.5.3/docs/lib/passlib.context-interface.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.context-interface.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -.. index:: CryptContext; interface - -.. _cryptcontext-interface: - -=============================================== -:mod:`passlib.context` - Module Contents -=============================================== - -.. currentmodule:: passlib.context - -This details all the constructors and methods provided by :class:`!CryptContext` -and :class:`!CryptPolicy`. - -.. seealso:: - - * :doc:`passlib.context-usage` - - * :doc:`passlib.context-options` - -The Context Object -================== -.. autoclass:: CryptContext(schemes=None, policy=, \*\*kwds) - -The Policy Object -================= -.. autoclass:: CryptPolicy(\*\*kwds) - -Other Helpers -============= -.. autoclass:: LazyCryptContext([schemes=None,] **kwds [, create_policy=None]) diff -Nru passlib-1.5.3/docs/lib/passlib.context-options.rst passlib-1.6.1/docs/lib/passlib.context-options.rst --- passlib-1.5.3/docs/lib/passlib.context-options.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.context-options.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,241 +0,0 @@ -.. index:: CryptContext; constructor options - -.. _cryptcontext-options: - -============================================= -:mod:`passlib.context` - Constructor Options -============================================= - -.. currentmodule:: passlib.context - -The :class:`CryptContext` accepts a number of keyword options. -These are divides into the "context options", which affect -the context instance directly, and the "hash options", -which affect the context treats a particular type of hash: - -.. seealso:: - - * :doc:`passlib.context-usage` - - * :doc:`passlib.context-interface` - -Context Options -=============== -The following keyword options are accepted by both the :class:`CryptContext` -and :class:`CryptPolicy` constructors, and directly affect the behavior -of the :class:`!CryptContext` instance itself: - -``schemes`` - List of handler names and/or instances which the CryptContext should recognize. - This is usually required. - - For use in INI files, this may also be specified as a single comma-separated string - of handler names. - - Potential names can include the name of any class importable from the :mod:`passlib.hash` module. - For example, to specify the :class:`passlib.hash.sha256_crypt` and the :class:`passlib.hash.des_crypt` schemes - should be supported for your new context:: - - >>> myctx = CryptContext(schemes=["sha256_crypt", "des_crypt"]) - -``deprecated`` - - List of handler names which should be considered deprecated by the CryptContext. - This should be a subset of the names of the handlers listed in schemes. - This is optional, if not specified, no handlers will be considered deprecated. - - For use in INI files, this may also be specified as a single comma-separated string - of handler names. - - This is primarily used by :meth:`CryptContext.hash_needs_update` and :meth:`CryptPolicy.handler_is_deprecated`. - If the application does not use these methods, this option can be ignored. - - Example: ``deprecated=["des_crypt"]``. - -``default`` - - Specifies the name of the default handler to use when encrypting a new password. - If no default is specified, the first handler listed in ``schemes`` will be used. - Any name specified *must* be in the list of supported schemes (see the ``schemes`` kwd). - - Example: ``default="sha256_crypt"``. - -``min_verify_time`` - - If specified, all :meth:`CryptContext.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 through timing measurements. - - Specified in integer or fractional seconds. - - Example: ``min_verify_time=0.1``. - -.. note:: - - For symmetry with the format of the hash option keywords (below), - all of the above context option keywords may also be specified - using the format :samp:`context__{option}` (note double underscores), - or :samp:`context.{option}` within INI files. - -.. note:: - - To override context options for a particular :ref:`user category `, - use the format :samp:`{category}__context__{option}`, - or :samp:`{category}.context.{option}` within an INI file. - -Hash Options -============ -The following keyword options are accepted by both the :class:`CryptContext` -and :class:`CryptPolicy` constructors, and affect how a :class:`!CryptContext` instance -treats hashes belonging to a particular hash scheme, as identified by the hash's handler name. - -All hash option keywords should be specified using the format :samp:`{hash}__{option}` -(note double underscores); where :samp:`{hash}` is the name of the hash's handler, -and :samp:`{option}` is the name of the specific options being set. -Within INI files, this may be specified using the alternate format :samp:`{hash}.{option}`. - -:samp:`{hash}__default_rounds` - - Sets the default number of rounds to use when generating new hashes (via :meth:`CryptContext.encrypt`). - - If not set, this will use max rounds hash option (see below), - or fall back to the algorithm-specified default. - For hashes which do not support a rounds parameter, this option is ignored. - -:samp:`{hash}__vary_rounds` - - if specified along with :samp:`{hash}__default_rounds`, - this will cause each new hash created by :meth:`CryptContext.encrypt` - to have a rounds value random chosen from the range :samp:`{default_rounds} +/- {vary_rounds}`. - - this may be specified as an integer value, or as a string containing an integer - with a percent suffix (eg: ``"10%"``). if specified as a percent, - the amount varied will be calculated as a percentage of the :samp:`{default_rounds}` value. - - The default passlib policy sets this to ``"10%"``. - - .. note:: - - If this is specified as a percentage, and the hash algorithm - uses a logarithmic rounds parameter, the amount varied - will be calculated based on the effective number of linear rounds, - not the actual rounds value. - This allows ``vary_rounds`` to be given a default value for all hashes - within a context, and behave sanely for both linear and logarithmic rounds parameters. - -:samp:`{hash}__min_rounds`, :samp:`{hash}__max_rounds` - - Place limits on the number of rounds allowed for a specific hash. - ``min_rounds`` defaults to 0, ``max_rounds`` defaults to unlimited. - - When encrypting new passwords with the specified hash (via :meth:`CryptContext.encrypt`), - the number of rounds will be clipped to these boundaries. - When checking for out-of-date hashes (via :meth:`CryptContext.hash_needs_update`), - it will flag any whose rounds are outside the range specified as needing to be re-encrypted. - For hashes which do not support a rounds parameter, these options are ignored. - - .. note:: - - These are configurable per-context limits, - they will be clipped by any hard limits set in the hash algorithm itself. - -:samp:`{hash}__{setting}` - - Any other option values, which match the name of a parameter listed - in the hash algorithm's ``handler.setting_kwds`` attribute, - will be passed directly to that hash whenever :meth:`CryptContext.encrypt` is called. - - 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. - -.. note:: - - Default options which will be applied to all hashes within the context - can be specified using the special hash name ``all``. For example, ``all__vary_rounds="10%"`` - would set the ``vary_rounds`` option to ``"10%"`` for all hashes, unless - it was overridden for a specific hash, such as by specifying ``sha256_crypt__vary_rounds="5%"``. - This feature is generally only useful for the ``vary_rounds`` hash option. - -.. _user-categories: - -User Categories -=============== -CryptContext offers an optional feature of "user categories": - -User categories take the form of a string (eg: ``admin`` or ``guest``), -passed to the CryptContext when one of it's methods is called. -These may be set by an application to indicate the hash belongs -to a user account which should be treated according to a slightly -different set of configuration options from normal user accounts; -this may involve requiring a stronger hash scheme, a larger -number of rounds for that scheme, or just a longer verify time. - -If an application wishes to use this feature, it all that is needed -is to prefix the name of any hash or context options with the name -of the category string it wants to use, and add an additional separator to the keyword: -:samp:`{category}__{hash}__{option}`` or ``{category}__context__{option}``. - -.. note:: - - For implementation & predictability purposes, - the context option ``schemes`` cannot be overridden per-category, - though all other options are allowed. In most cases, - the need to use a different hash for a particular category - can instead be acheived by overridden the ``default`` context option. - -Default Policy -============== -PassLib defines a library-default policy, providing (hopefully) sensible defaults for new 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 default policy object may be imported as :data:`passlib.context.default_policy`, -or viewed in the source code under ``$SOURCE/passlib/default.cfg``. - -Sample Policy File -================== -A sample policy file: - -.. code-block:: ini - - [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%% - ; NOTE the '%' above has to be escaped due to configparser interpolation - - #setup some hash-specific defaults - sha512_crypt.min_rounds = 40000 - bcrypt.min_rounds = 10 - - #create a "admin" category, which uses bcrypt by default, and has stronger hashes - admin.context.default = bcrypt - admin.sha512_crypt.min_rounds = 100000 - admin.bcrypt.min_rounds = 13 - -And the equivalent as a set of python keyword options:: - - dict( - #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 "admin" category, which uses bcrypt by default, and has stronger hashes - admin__context__default = bcrypt - admin__sha512_crypt__min_rounds = 100000 - admin__bcrypt__min_rounds = 13 - ) diff -Nru passlib-1.5.3/docs/lib/passlib.context-tutorial.rst passlib-1.6.1/docs/lib/passlib.context-tutorial.rst --- passlib-1.5.3/docs/lib/passlib.context-tutorial.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.context-tutorial.rst 2012-08-01 17:05:50.000000000 +0000 @@ -0,0 +1,560 @@ +.. 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 :mod:`passlib.context` module contains one main class: :class:`!passlib.context.CryptContext`. +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 ` 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:`~passlib.ifc.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 `, +:doc:`md5_crypt `, and +:doc:`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, and hashing and verifying passwords is equally as straightforward:: + + >>> # this loads first algorithm in the schemes list (sha256_crypt), + >>> # generates a new salt, and hashes the password: + >>> hash1 = myctx.encrypt("joshua") + >>> hash1 + '$5$rounds=80000$HFEGd1wnFknpibRl$VZqjyYcTenv7CtOf986hxuE0pRaGXnuLXyfb7m9xL69' + + >>> # when verifying a password, the algorithm is identified automatically: + >>> myctx.verify("gtnw", hash1) + False + >>> myctx.verify("joshua", hash1) + True + + >>> # alternately, you can explicitly pick one of the configured algorithms, + >>> # through this is rarely needed in practice: + >>> hash2 = myctx.encrypt("dogsnamehere", scheme="md5_crypt") + >>> hash2 + '$1$e2nig/AC$stejMS1ek6W0/UogYKFao/' + + >>> myctx.verify("letmein", hash2) + False + >>> myctx.verify("dogsnamehere", hash2) + True + +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' + +This concludes the basics of how to use a CryptContext object. +The rest of the sections detail the various features it offers, +which probably provide a better argument for *why* you'd want to use it. + +.. seealso:: + + * the :meth:`CryptContext.encrypt`, :meth:`~CryptContext.verify`, and :meth:`~CryptContext.identify` methods. + * the :ref:`schemes ` and :ref:`default ` 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 ` +algorithm supports a ``rounds`` parameter which defaults to 80000, +and the :class:`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 ` and + :ref:`per-scheme setting ` 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 `_-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 `, + :ref:`min_rounds `, + and :ref:`max_rounds ` 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 (e.g. ``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 the purposes of this example (and the sample config file listed above), + it's assumed this value will be ``None`` for most users, and ``"admin"`` for special users. + This namespace is entirely up to the application, it just has to match the + category names used in the config 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 ... diff -Nru passlib-1.5.3/docs/lib/passlib.context-usage.rst passlib-1.6.1/docs/lib/passlib.context-usage.rst --- passlib-1.5.3/docs/lib/passlib.context-usage.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.context-usage.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,317 +0,0 @@ -.. index:: CryptContext; usage examples - -.. _cryptcontext-examples: - -==================================================== -:mod:`passlib.context` - Usage Examples -==================================================== - -.. currentmodule:: passlib.context - -This section gives examples on how to use the :class:`CryptContext` object -for a number of different use cases. - -.. seealso:: - - * :doc:`passlib.context-interface` - - * :doc:`passlib.context-options` - -Basic Usage -=========== -To start off with a simple example of how to create and use a CryptContext:: - - >>> 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) - - -.. _using-predefined-contexts: - -Using Predefined CryptContexts -============================== -Passlib contains a number of pre-made :class:`!CryptContext` instances, -configured for various purposes -(see :mod:`passlib.apps` and :mod:`passlib.hosts`). -These can be used directly by importing them from passlib, -such as the following example: - - >>> from passlib.apps import ldap_context as pwd_context - >>> pwd_context.encrypt("somepass") - '{SSHA}k4Ap0wYJWMrkgNhptlntsPGETBBwEoSH' - -However, applications which use the predefined contexts will frequently -find they need to modify the context in some way, such as selecting -a different default hash scheme. This is best done by importing -the original context, and then making an application-specific -copy; using the :meth:`CryptContext.replace` method to create -a mutated copy of the original object:: - - >>> from passlib.apps import ldap_context - >>> pwd_context = ldap_context.replace(default="ldap_md5_crypt") - >>> pwd_context.encrypt("somepass") - '{CRYPT}$1$Cw7t4sbP$dwRgCMc67mOwwus9m33z71' - -Examining a CryptContext Instance -================================= -All configuration options for a :class:`!CryptContext` instance -are stored in a :class:`!CryptPolicy` instance accessible through -the :attr:`CryptContext.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() - - -See the :class:`CryptPolicy` class for more details on it's interface. - -Full Integration Example -======================== -The following is an extended example of how PassLib can be integrated into an existing -application to provide runtime policy changes, deprecated hash migration, -and other features. This is example uses a lot of different features, -and many developers will want to pick and choose what they need from this example. -The totality of this example is overkill for most simple applications. - -Policy Configuration File -------------------------- - -While it is possible to create a CryptContext instance manually, or to import an existing one, -applications with advanced policy requirements may want to create a hash policy file -(options show below are detailed in :ref:`cryptcontext-options`): - -.. 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_sha1, along with legacy md5_crypt hashes: - schemes = pbkdf2_sha1, md5_crypt - - ;flag md5_crypt as deprecated - ; (existing md5_crypt hashes will be flagged as needs-updating) - deprecated = md5_crypt - - ;set verify to always take at least 1/10th of a second - min_verify_time = 0.1 - - ;set boundaries for pbkdf2 rounds parameter - ; (pbkdf2 hashes outside this range will be flagged as needs-updating) - pbkdf2_sha1.min_rounds = 10000 - pbkdf2_sha1.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 %. - pbkdf2_sha1.default_rounds = 20000 - pbkdf2_sha1.vary_rounds = 10%% - ; NOTE the '%' above has to be doubled due to configparser interpolation - - ;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 = 40000 - admin.pbkdf2_sha1.default_rounds = 50000 - -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 - from passlib.context import CryptPolicy - - def myapp_startup(): - - # - # ... other code ... - # - - # vars: - # policy_path - path to policy file defined in previous step - # - user_pwd_context.policy = CryptPolicy.from_path(policy_path) - - # - #if you want to reconfigure the context without restarting the application, - #simply repeat the above step at another point. - # - - # - # ... other code ... - # - -.. _context-encrypting-passwords: - -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. - -.. _context-verifying-passwords: - -Verifying 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 ... - # - - # - #vars: - # 'hash' containing the specified user's hash, - # 'secret' containing the putative password - # 'category' containing a category assigned to the user account - # - #see note in "Encrypting New Passwords" about the category kwd - # - - ok = user_pwd_context.verify(secret, hash, category=category) - if not ok: - #... password did not match. do mean things ... - pass - - else: - #... password matched ... - #... do successful login actions ... - pass - -.. _context-migrating-passwords: - -Verifying & Migrating Existing Passwords ----------------------------------------- -The CryptContext object offers the ability to deprecate schemes, -set lower strength bounds, and then flag any existing hashes which -violate these limits. -Applications which want to re-encrypt any deprecated hashes -found in their database should use the following template -instead of the one found in the previous step:: - - 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... - #given the following variables: - # - #vars: - # 'hash' containing the specified user's hash, - # 'secret' containing the putative password - # 'category' containing a category assigned to the user account - # - #see note in "Encrypting New Passwords" about the category kwd - # - - 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 ... diff -Nru passlib-1.5.3/docs/lib/passlib.context.rst passlib-1.6.1/docs/lib/passlib.context.rst --- passlib-1.5.3/docs/lib/passlib.context.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.context.rst 2012-07-13 23:31:35.000000000 +0000 @@ -1,43 +1,425 @@ -.. index:: CryptContext; usage examples, CryptContext; overview +.. index:: CryptContext; reference -.. _cryptcontext-overview: +.. currentmodule:: passlib.context -============================================== -:mod:`passlib.context` - CryptContext Overview -============================================== - -.. module:: passlib.context - :synopsis: CryptContext class for managing multiple password hash schemes - -Motivation -========== -Though there is a wide range of password hashing schemes, -within a specific context (like a linux "shadow" file) -only a select list of schemes will be used. -As time goes on, new schemes are added and made the default, -the strength of existing schemes is tweaked, and other schemes are deprecated entirely. -Throughout all this, existing password hashes that don't comply -with the new policies must be detected and rehashed using the -new default configuration. In order to automate as much of these tasks as possible, -this module provides the :class:`CryptContext` class. - -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 scheme. While contexts can be created explicitly, -Passlib also offers a number of predefined :class:`!CryptContext` instances -which can be used out-of-the box (see :mod:`passlib.apps` and :mod:`passlib.hosts`), -or :ref:`modified ` to suit the application. - -Subtopics -========= -New users should see the usage examples -in the next section to get a feel for how the :class:`!CryptContext` class works. - -.. toctree:: - :maxdepth: 1 - - passlib.context-usage - passlib.context-interface - passlib.context-options +.. _context-reference: + +====================================================== +:mod:`passlib.context` - CryptContext Reference +====================================================== +This page provides a complete reference of all the methods +and options supported by the :class:`!CryptContext` class +and helper utilities. + +.. seealso:: + + * :ref:`CryptContext Overview & Tutorial ` -- + overview of this class and walkthrough of how to use it. + +The CryptContext Class +====================== +.. class:: CryptContext(schemes=None, \*\*kwds) + + Helper for encrypting passwords using different algorithms. + + At it's base, this is a proxy object that makes it easy to use + multiple :class:`~passlib.ifc.PasswordHash` objects at the same time. + Instances of this class can be created by calling the constructor + with the appropriate keywords, or by using one of the alternate + constructors, which can load directly from a string or a local file. + Since this class has so many options and methods, they have been broken + out into subsections: + + * `Constructor Keywords`_ -- all the keywords this class accepts. + - `Context Options`_ -- options affecting the Context itself. + - `Algorithm Options`_ -- options controlling the wrapped hashes. + * `Primary Methods`_ -- the primary methods most applications need. + * `Hash Migration`_ -- methods for automatically replacing deprecated hashes. + * `Alternate Constructors`_ -- creating instances from strings or files. + * `Changing the Configuration`_ -- altering the configuration of an existing context. + * `Examining the Configuration`_ -- programmatically examining the context's settings. + * `Saving the Configuration`_ -- exporting the context's current configuration. + +.. index:: CryptContext; keyword options + +.. rst-class:: html-toggle expanded emphasized + +Constructor Keywords +-------------------- +The :class:`CryptContext` class accepts the following keywords, +all of which are optional. +The keywords are divided into two categories: `context options`_, which affect +the CryptContext itself; and `algorithm options`_, which place defaults +and limits on the algorithms used by the CryptContext. + +.. _context-options: + +Context Options +............... +Options which directly affect the behavior of the CryptContext instance: + +.. _context-schemes-option: + +``schemes`` + List of algorithms which the instance should support. + + The most important option in the constructor, + This option controls what hashes can be used + by the :meth:`~CryptContext.encrypt` method, + which hashes will be recognized by :meth:`~CryptContext.verify` + and :meth:`~CryptContext.identify`, and other effects + throughout the instance. + It should be a sequence of names, + drawn from the hashes in :mod:`passlib.hash`. + Listing an unknown name will cause a :exc:`ValueError`. + You can use the :meth:`~CryptContext.schemes` method + to get a list of the currently configured algorithms. + As an example, the following creates a CryptContext instance + which supports the :class:`~passlib.hash.sha256_crypt` and + :class:`~passlib.hash.des_crypt` schemes:: + + >>> from passlib.context import CryptContext + >>> myctx = CryptContext(schemes=["sha256_crypt", "des_crypt"]) + >>> myctx.schemes() + ("sha256_crypt", "des_crypt") + + + .. note:: + + The order of the schemes is sometimes important, + as :meth:`~CryptContext.identify` will run + through the schemes from first to last until an algorithm + "claims" the hash. So plaintext algorithms and + the like should be listed at the end. + + .. seealso:: the :ref:`context-basic-example` example in the tutorial. + +.. _context-default-option: + +``default`` + Specifies the name of the default scheme. + + This option controls which of the configured + schemes will be used as the default when encrypting + new hashes. This parameter is optional; if omitted, + the first non-deprecated algorithm in ``schemes`` will be used. + You can use the :meth:`~CryptContext.default_scheme` method + to retreive the name of the current default scheme. + As an example, the following demonstrates the effect + of this parameter on the :meth:`~CryptContext.encrypt` + method:: + + >>> from passlib.context import CryptContext + >>> myctx = CryptContext(schemes=["sha256_crypt", "md5_crypt"]) + + >>> # encrypt() uses the first scheme + >>> myctx.default_scheme() + 'sha256_crypt' + >>> myctx.encrypt("password") + '$5$rounds=80000$R5ZIZRTNPgbdcWq5$fT/Oeqq/apMa/0fbx8YheYWS6Z3XLTxCzEtutsk2cJ1' + + >>> # but setting default causes the second scheme to be used. + >>> myctx.update(default="md5_crypt") + >>> myctx.default_scheme() + 'md5_crypt' + >>> myctx.encrypt("password") + '$1$Rr0C.KI8$Kvciy8pqfL9BQ2CJzEzfZ/' + + .. seealso:: the :ref:`context-basic-example` example in the tutorial. + +.. _context-deprecated-option: + +``deprecated`` + List of algorithms which should be considered "deprecated". + + This has the same format as ``schemes``, and should be + a subset of those algorithms. The main purpose of this + method is to flag schemes which need to be rehashed + when the user next logs in. This has no effect + on the `Primary Methods`_; but if the special `Hash Migration`_ + methods are passed a hash belonging to a deprecated scheme, + they will flag it as needed to be rehashed using + the ``default`` scheme. + + This may also contain a single special value, + ``["auto"]``, which will configure the CryptContext instance + to deprecate *all* supported schemes except for the default scheme. + + .. seealso:: :ref:`context-migration-example` in the tutorial + +.. _context-min-verify-time-option: + +``min_verify_time`` + + If specified, unsuccessful :meth:`~CryptContext.verify` + calls will be penalized, and take at least this may + seconds before the method returns. May be an integer + or fractional number of seconds. + + .. deprecated:: 1.6 + This option has not proved very useful, and will + be removed in version 1.8. + +.. _context-algorithm-options: + +Algorithm Options +................. +All of the other options that can be passed to a :class:`CryptContext` +constructor affect individual hash algorithms. +All of the following keys have the form :samp:`{scheme}__{key}`, +where :samp:`{scheme}` is the name of one of the algorithms listed +in ``schemes``, and :samp:`{option}` one of the parameters below: + +.. _context-default-rounds-option: + +:samp:`{scheme}__default_rounds` + + Sets the default number of rounds to use with this scheme + when generating new hashes (using :meth:`~CryptContext.encrypt`). + + If not set, this will fall back to the an algorithm-specific + :attr:`~passlib.ifc.PasswordHash.default_rounds`. + For hashes which do not support a rounds parameter, this option is ignored. + As an example:: + + >>> from passlib.context import CryptContext + + >>> # no explicit default_rounds set, so encrypt() uses sha256_crypt's default (80000) + >>> myctx = CryptContext(["sha256_crypt"]) + >>> myctx.encrypt("fooey") + '$5$rounds=80000$60Y7mpmAhUv6RDvj$AdseAOq6bKUZRDRTr/2QK1t38qm3P6sYeXhXKnBAmg0' + ^^^^^ + + >>> # but if a default is specified, it will be used instead. + >>> myctx = CryptContext(["sha256_crypt"], sha256_crypt__default_rounds=77123) + >>> myctx.encrypt("fooey") + '$5$rounds=77123$60Y7mpmAhUv6RDvj$AdseAOq6bKUZRDRTr/2QK1t38qm3P6sYeXhXKnBAmg0' + ^^^^^ + + .. seealso:: the :ref:`context-default-settings-example` example in the tutorial. + +:samp:`{scheme}__vary_rounds` + + Instead of using a fixed rounds value (such as specified by + ``default_rounds``, above); this option will cause each call + to :meth:`~CryptContext.encrypt` to vary the default rounds value + by some amount. + + This can be an integer value, in which case each call will use a rounds + value within the range ``default_rounds +/- vary_rounds``. It may + also be a floating point value within the range 0.0 .. 1.0, + in which case the range will be calculated as a proportion of the + current default rounds (``default_rounds +/- default_rounds*vary_rounds``). + A typical setting is ``0.1`` to ``0.2``. + + As an example of how this parameter operates:: + + >>> # without vary_rounds set, encrypt() uses the same amount each time: + >>> from passlib.context import CryptContext + >>> myctx = CryptContext(schemes=["sha256_crypt"], + ... sha256_crypt__default_rounds=80000) + >>> myctx.encrypt("fooey") + '$5$rounds=80000$60Y7mpmAhUv6RDvj$AdseAOq6bKUZRDRTr/2QK1t38qm3P6sYeXhXKnBAmg0' + >>> myctx.encrypt("fooey") + '$5$rounds=80000$60Y7mpmAhUv6RDvj$AdseAOq6bKUZRDRTr/2QK1t38qm3P6sYeXhXKnBAmg0' + ^^^^^ + + >>> # but if vary_rounds is set, each one will be randomized + >>> # (in this case, within the range 72000 .. 88000) + >>> myctx = CryptContext(schemes=["sha256_crypt"], + ... sha256_crypt__default_rounds=80000, + ... sha256_crypt__vary_rounds=0.1) + >>> myctx.encrypt("fooey") + '$5$rounds=83966$bMpgQxN2hXo2kVr4$jL4Q3ov41UPgSbO7jYL0PdtsOg5koo4mCa.UEF3zan.' + >>> myctx.encrypt("fooey") + '$5$rounds=72109$43BBHC/hYPHzL69c$VYvVIdKn3Zdnvu0oJHVlo6rr0WjiMTGmlrZrrH.GxnA' + ^^^^^ + + .. note:: + + This is not a *needed* security measure, but it lets some of the less-significant + digits of the rounds value act as extra salt bits; and helps foil + any attacks targeted at a specific number of rounds of a hash. + +.. _context-min-rounds-option: +.. _context-max-rounds-option: + +:samp:`{scheme}__min_rounds`, +:samp:`{scheme}__max_rounds` + + These options place a limit on the number of rounds allowed for a particular + scheme. + + For one, they limit what values are allowed for ``default_rounds``, + and clip the effective range of the ``vary_rounds`` parameter. + More importantly though, they proscribe a minimum strength for the hash, + and any hashes which don't have sufficient rounds will be flagged as + needing rehashing by the `Hash Migration`_ methods. + + .. note:: + + These are configurable per-context limits. + A warning will be issued if they exceed any hard limits + set by the algorithm itself. + + .. seealso:: the :ref:`context-min-rounds-example` example in the tutorial. + +.. _context-other-option: + +:samp:`{scheme}__{other-option}` + + Finally, any other options are assumed to correspond to one of the + that algorithm's :meth:`!encrypt` :attr:`settings <~passlib.ifc.PasswordHash.setting_kwds>`, + such as setting a ``salt_size``. + + .. seealso:: the :ref:`context-default-settings-example` example in the tutorial. + +Global Algorithm Options +........................ +:samp:`all__{option}` + + The special scheme ``all`` permits you to set an option, and have + it act as a global default for all the algorithms in the context. + For instance, ``all__vary_rounds=0.1`` would set the ``vary_rounds`` + option for all the schemes where it was not overridden with an + explicit :samp:`{scheme}__vary_rounds` option. + +.. _user-categories: + +.. rst-class:: html-toggle + +User Categories +............... +:samp:`{category}__context__{option}`, +:samp:`{category}__{scheme}__{option}` + + Passing keys with this format to the :class:`CryptContext` constructor + allows you to specify conditional context and algorithm options, + controlled by the ``category`` parameter supported by most CryptContext + methods. + + These options are conditional because they only take effect if + the :samp:`{category}` prefix of the option matches the value of the ``category`` + parameter of the CryptContext method being invoked. In that case, + they override any options specified without a category + prefix (e.g. `admin__sha256_crypt__min_rounds` would override + `sha256_crypt__min_rounds`). + The category prefix and the value passed into the ``category`` parameter + can be any string the application wishes to use, the only constraint + is that ``None`` indicates the default category. + +*Motivation:* +Policy limits such as default rounds values and deprecated schemes +generally have to be set globally. However, it's frequently desirable +to specify stronger options for certain accounts (such as admin accounts), +choosing to sacrifice longer hashing time for a more secure password. +The user categories system allows for this. +For example, a CryptContext could be set up as follows:: + + >>> # A context object can be set up as follows: + >>> from passlib.context import CryptContext + >>> myctx = CryptContext(schemes=["sha256_crypt"], + ... sha256_crypt__default_rounds=77000, + ... staff__sha256_crypt__default_rounds=88000) + + >>> # In this case, calling encrypt with ``category=None`` would result + >>> # in a hash that used 77000 sha256-crypt rounds: + >>> myctx.encrypt("password", category=None) + '$5$rounds=77000$sj3XI0AbKlEydAKt$BhFvyh4.IoxaUeNlW6rvQ.O0w8BtgLQMYorkCOMzf84' + ^^^^^ + + >>> # But if the application passed in ``category="staff"`` when an administrative + >>> # account set their password, 88000 rounds would be used: + >>> myctx.encrypt("password", category="staff") + '$5$rounds=88000$w7XIdKfTI9.YLwmA$MIzGvs6NU1QOQuuDHhICLmDsdW/t94Bbdfxdh/6NJl7' + ^^^^^ + +.. rst-class:: html-toggle expanded emphasized + +Primary Methods +--------------- +The main interface to the CryptContext object deliberate mirrors +the :ref:`PasswordHash ` interface, since it's central +purpose is to act as a container for multiple password hashes. +Most applications will only need to make use two methods in a CryptContext +instance: + +.. automethod:: CryptContext.encrypt +.. automethod:: CryptContext.verify +.. automethod:: CryptContext.identify + +.. rst-class:: html-toggle + +"crypt"-style methods +..................... +Additionally, the main interface offers wrappers for the two Unix "crypt" +style methods provided by all the :class:`~passlib.ifc.PasswordHash` objects: + +.. automethod:: CryptContext.genhash +.. automethod:: CryptContext.genconfig + +.. rst-class:: html-toggle expanded emphasized + +Hash Migration +-------------- +Applications which want to detect and re-encrypt deprecated +hashes will want to use one of the following methods: + + +.. automethod:: CryptContext.verify_and_update +.. automethod:: CryptContext.needs_update +.. automethod:: CryptContext.hash_needs_update + +.. rst-class:: html-toggle expanded emphasized + +Alternate Constructors +---------------------- +In addition to the main class constructor, which accepts a configuration +as a set of keywords, there are the following alternate constructors: + +.. automethod:: CryptContext.from_string +.. automethod:: CryptContext.from_path +.. automethod:: CryptContext.copy + +.. rst-class:: html-toggle expanded emphasized + +Changing the Configuration +-------------------------- +:class:`CryptContext` objects can have their configuration replaced or updated +on the fly, and from a variety of sources (keywords, strings, files). +This is done through three methods: + +.. automethod:: CryptContext.update(\*\*kwds) +.. automethod:: CryptContext.load +.. automethod:: CryptContext.load_path + +.. rst-class:: html-toggle expanded emphasized + +Examining the Configuration +--------------------------- +The CryptContext object also supports basic inspection of it's +current configuration: + +.. automethod:: CryptContext.schemes +.. automethod:: CryptContext.default_scheme +.. automethod:: CryptContext.handler + +.. rst-class:: html-toggle expanded emphasized + +Saving the Configuration +------------------------ +More detailed inspection can be done by exporting the configuration +using one of the serialization methods: + +.. automethod:: CryptContext.to_dict +.. automethod:: CryptContext.to_string + +Other Helpers +============= +.. autoclass:: LazyCryptContext([schemes=None,] \*\*kwds [, onload=None]) + +.. rst-class:: html-toggle + +The CryptPolicy Class (deprecated) +================================== +.. autoclass:: CryptPolicy diff -Nru passlib-1.5.3/docs/lib/passlib.exc.rst passlib-1.6.1/docs/lib/passlib.exc.rst --- passlib-1.5.3/docs/lib/passlib.exc.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.exc.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,32 @@ +=============================================================== +:mod:`passlib.exc` - exceptions and warnings raised by Passlib +=============================================================== + +.. module:: passlib.exc + :synopsis: exceptions & warnings raised by Passlib + +This module contains all the custom exceptions & warnings that +may be raised by Passlib. + +Exceptions +========== +.. autoexception:: MissingBackendError + +.. index:: + pair: environmental variable; PASSLIB_MAX_PASSWORD_SIZE + +.. autoexception:: PasswordSizeError + +Warnings +======== +.. autoexception:: PasslibWarning + +Minor Warnings +-------------- +.. autoexception:: PasslibConfigWarning +.. autoexception:: PasslibHashWarning + +Critical Warnings +----------------- +.. autoexception:: PasslibRuntimeWarning +.. autoexception:: PasslibSecurityWarning diff -Nru passlib-1.5.3/docs/lib/passlib.ext.django.rst passlib-1.6.1/docs/lib/passlib.ext.django.rst --- passlib-1.5.3/docs/lib/passlib.ext.django.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.ext.django.rst 2012-05-03 16:36:58.000000000 +0000 @@ -1,151 +1,172 @@ -.. index:: django; password hashing app - -================================================== -:mod:`passlib.ext.django` - Django Password Helper -================================================== +.. index:: Django; password hashing plugin .. module:: passlib.ext.django +========================================================== +:mod:`passlib.ext.django` - Django Password Hashing Plugin +========================================================== + +.. versionadded:: 1.6 + +This module contains a `Django `_ plugin which +overriddes all of Django's password hashing functions, replacing them +with wrappers around a Passlib :ref:`CryptContext ` object +whose configuration is controled from Django's ``settings``. +While this extension's utility is diminished with the advent +of Django 1.4's *hashers* framework, this plugin still has a number +of uses: + +* Make use of the new Django 1.4 :ref:`pbkdf2 & bcrypt formats `, + even under earlier Django releases. + +* Allow your application to work with any password hash format + :doc:`supported ` by Passlib, allowing you to import + existing hashes from other systems. + Common examples include SHA512-Crypt, PHPass, and BCrypt. + +* Set different iterations / cost settings based on the type of user account, + and automatically update hashes that use weaker settings when the user + logs in. + +* Mark any hash algorithms as deprecated, and automatically migrate to stronger + hashes when the user logs in. + .. warning:: - This module is currently under development. - It works and has good unittest coverage, - but has not seen very much real-world use; - *caveat emptor*. - -.. todo:: - - This documentation needs to be cleaned up significantly - for new users. - -Overview -======== -This module is intended for use with -`Django `_-based web applications. -It contains a Django app which allows you to override -Django's builtin password hashing routine -with any Passlib :doc:`CryptContext ` instance. -By default, it comes configured to add support for -:class:`~passlib.hash.sha512_crypt`, and will automatically -upgrade all existing Django password hashes as your users log in. - -:doc:`SHA512-Crypt ` -was chosen as the best choice for the average Django deployment: -accelerated implementations are available on most stock Linux systems, -as well as Google App Engine, and Passlib provides a pure-python -fallback for all other platforms. + This plugin should be considered "release candidate" quality. + It works, and has good unittest coverage, but has seen only + limited real-world use. Please report any issues. + It has been tested with Django 0.9.6 - 1.4. Installation ============= -Installation is simple: just add ``"passlib.ext.django"`` to -Django's ``settings.INSTALLED_APPS``. This app will handle -everything else. - -Once done, when this app is imported by Django, -it will automatically monkeypatch -:class:`!django.contrib.auth.models.User` -to use a Passlib :class:`~passlib.context.CryptContext` instance -in place of the normal Django password authentication routines. - -This provides hash migration, the ability to set stronger policies -for superuser & staff passwords, and stronger password hashing schemes. +Installation is simple: once Passlib itself has been installed, just add +``"passlib.ext.django"`` to Django's ``settings.INSTALLED_APPS``, +as soon as possible after ``django.contrib.auth``. + +Once installed, this plugin will automatically monkeypatch +Django to use a Passlib :class:`!CryptContext` +instance in place of the normal Django password authentication routines +(as an unfortunate side effect, this disables Django 1.4's hashers framework entirely, +though the default configuration supports all the built-in Django 1.4 hashers). Configuration ============= -Once installed, you can set the following options in django ``settings.py``: +While this plugin will function perfectly well without setting any configuration +options, you can customize it using the following options in Django's ``settings.py``: -``PASSLIB_CONTEXT`` - This may be one of a number of values: +``PASSLIB_CONFIG`` + + This option specifies the CryptContext configuration options + that will be used when the plugin is loaded. + + * It's value will usually be an INI-formatted string or a dictionary, containing + options to be passed to :class:`~passlib.context.CryptContext`. + + * Alternately, it can be the name of any preset supported by + :func:`~passlib.ext.django.utils.get_preset_config`, such as + ``"passlib-default"`` or ``"django-default"``. + + * Finally, it can be the special string ``"disabled"``, which will disable + this plugin. + + + At any point after this plugin has been loaded, you can serialize + it's current configuration to a string:: + + >>> from passlib.ext.django.models import password_context + >>> print password_context.to_string() + + This string can then be modified, and used as the new value + of ``PASSLIB_CONFIG``. + + .. note:: - * The string ``"passlib-default"``, which will cause Passlib - to replace Django's hash routines with a builtin policy - that supports all existing django hashes; but as users - log in, upgrades them all to :class:`~passlib.hash.pbkdf2_sha256`. - It also supports stronger hashing for the superuser account. - - This is the default behavior if ``PASSLIB_CONTEXT`` is not set. - - The exact default policy used can be found in - :data:`~passlib.ext.django.utils.DEFAULT_CTX`. - - * ``None``, in which case this app will do nothing when Django is loaded. - - * A multiline configuration string suitable for passing to - :meth:`passlib.context.CryptPolicy.from_string`. - It is *strongly* recommended to use a configuration which will support - the existing Django hashes - (see :data:`~passlib.ext.django.utils.STOCK_CTX`). + It is *strongly* recommended to use a configuration which will support + the existing Django hashes. Dumping and then modifying one of the + preset strings is a good starting point. ``PASSLIB_GET_CATEGORY`` - By default, Passlib will invoke the specified context with a category - string that's dependant on the User instance. - superusers will be assigned to the ``superuser`` category, - staff to the ``staff`` category, and all other accounts - assigned to ``None``. - - This configuration option allows overriding that logic - by specifying an alternate function with the call signature - ``get_category(user) -> category|None``. - + By default, Passlib will assign users to one of three categories: + ``"superuser"``, ``"staff"``, or ``None``; based on the attributes + of the ``User`` object. This allows ``PASSLIB_CONFIG`` + to have per-category policies, such as a larger number of iterations + for the superuser account. + + This option allows overidding the function which performs this mapping, + so that more fine-grained / alternate user categories can be used. + If specified, the function should have the call syntax + ``get_category(user) -> category_string|None``. + .. seealso:: - See :ref:`user-categories` for more details about - the category system in Passlib. + See :ref:`user-categories` for more details. -Utility Functions -================= -.. module:: passlib.ext.django.utils +``PASSLIB_CONTEXT`` -Whether or not you install this application into Django, -the following utility functions are available for overriding -Django's password hashes: - -.. data:: DEFAULT_CTX - - This is a string containing the default hashing policy - that will be used by this application if none is specified - via ``settings.PASSLIB_CONTEXT``. - It defaults to the following:: + .. deprecated:: 1.6 + This is a deprecated alias for ``PASSLIB_CONFIG``, + used by the (undocumented) version of this plugin that was + released with Passlib 1.5. It should not be used by new applications. + +Module Contents +=============== +.. module:: passlib.ext.django.models + +.. data:: password_context + + The :class:`!CryptContext` instance that drives this plugin. + It can be imported and examined to inspect the current configuration, + changes made to it will immediately alter how Django hashes passwords. - [passlib] - schemes = - sha512_crypt, - django_salted_sha1, django_salted_md5, - django_des_crypt, hex_md5, - django_disabled +.. module:: passlib.ext.django.utils - default = sha512_crypt +.. autofunction:: get_preset_config - deprecated = - django_salted_sha1, django_salted_md5, - django_des_crypt, hex_md5 +.. data:: PASSLIB_DEFAULT + + This constant contains the default configuration for ``PASSLIB_CONFIG``. + It provides the following features: - all__vary_rounds = 5%% + * uses :class:`~passlib.hash.django_pbkdf2_sha256` as the default algorithm. + * supports all of the Django 1.0-1.4 :doc:`hash formats
`. + * additionally supports SHA512-Crypt, BCrypt, and PHPass. + * is configured to use a larger number of rounds for the superuser account. + * is configured to automatically migrate all Django 1.0 hashes + to use the default hash as soon as each user logs in. - sha512_crypt__default_rounds = 15000 - staff__sha512_crypt__default_rounds = 25000 - superuser__sha512_crypt__default_rounds = 35000 - -.. data:: STOCK_CTX - - This is a string containing the a hashing policy - which should be exactly the same as Django's default behavior. - It is mainly useful as a template for building off of - when defining your own custom hashing policy - via ``settings.PASSLIB_CONTEXT``. - It defaults to the following:: + As of Passlib 1.6, it contains the following string:: [passlib] - schemes = - django_salted_sha1, django_salted_md5, - django_des_crypt, hex_md5, - django_disabled - default = django_salted_sha1 + ; list of schemes supported by configuration + ; currently all django 1.4 hashes, django 1.0 hashes, + ; and three common modular crypt format hashes. + schemes = + django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, + django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5, + sha512_crypt, bcrypt, phpass - deprecated = hex_md5 + ; default scheme to use for new hashes + default = django_pbkdf2_sha256 -.. autofunction:: get_category + ; hashes using these schemes will automatically be re-hashed + ; when the user logs in (currently all django 1.0 hashes) + deprecated = + django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, + django_des_crypt, hex_md5 -.. autofunction:: set_django_password_context + ; sets some common options, including minimum rounds for two primary hashes. + ; if a hash has less than this number of rounds, it will be re-hashed. + all__vary_rounds = 0.05 + sha512_crypt__min_rounds = 80000 + django_pbkdf2_sha256__min_rounds = 10000 + + ; set somewhat stronger iteration counts for ``User.is_staff`` + staff__sha512_crypt__default_rounds = 100000 + staff__django_pbkdf2_sha256__default_rounds = 12500 + + ; and even stronger ones for ``User.is_superuser`` + superuser__sha512_crypt__default_rounds = 120000 + superuser__django_pbkdf2_sha256__default_rounds = 15000 diff -Nru passlib-1.5.3/docs/lib/passlib.hash.apr_md5_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.apr_md5_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.apr_md5_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.apr_md5_crypt.rst 2012-06-27 19:55:48.000000000 +0000 @@ -1,26 +1,31 @@ -.. index:: apache; md5 password hash +.. index:: Apache; md5 password hash ====================================================================== :class:`passlib.hash.apr_md5_crypt` - Apache's MD5-Crypt variant ====================================================================== +.. warning:: + + As of 2012-6-7, the MD5-Crypt algorithm is "no longer considered safe" + by it's author, who urges migration to newer hash algorithms. + .. currentmodule:: passlib.hash -This format is a variation of :class:`~passlib.hash.md5_crypt`, +This hash is a variation of :class:`~passlib.hash.md5_crypt`, primarily used by the Apache webserver in ``htpasswd`` files. -It contains only minor changes to md5-crypt, and should -be considered just as strong / weak as md5-crypt itself. +It contains only minor changes to the MD5-Crypt algorithm, +and should be considered just as weak as MD5-Crypt itself. -.. seealso:: :doc:`md5_crypt `, :mod:`passlib.apache` +.. seealso:: -Usage -===== -This algorithm can be used in exactly the same way as :class:`~passlib.hash.md5_crypt`, -see that class for details. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. + + * :mod:`passlib.apache` -- routines for manipulating ``htpasswd`` files. Interface ========= -.. autoclass:: apr_md5_crypt(checksum=None, salt=None, strict=False) +.. autoclass:: apr_md5_crypt() Format & Algorithm ================== @@ -34,6 +39,9 @@ Because of this change, even raw checksums generated by apr-md5-crypt and md5-crypt are not compatible with eachother. +See :doc:`md5_crypt ` for the format & algorithm +descriptions, as well as security notes. + .. rubric:: Footnotes .. [#] Apache's description of Apr-MD5-Crypt - diff -Nru passlib-1.5.3/docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst passlib-1.6.1/docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst --- passlib-1.5.3/docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst 2012-05-03 16:36:58.000000000 +0000 @@ -3,7 +3,7 @@ =========================================================================== .. index:: - pair: atlassian; pbkdf2 hash + pair: Atlassian; pbkdf2 hash .. currentmodule:: passlib.hash @@ -17,14 +17,11 @@ .. seealso:: - :doc:`passlib.hash.pbkdf2_digest ` - for some other PBKDF2-based hashes. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. -Usage -===== -These classes support both rounds and salts, -and can be used in the exact same manner -as :doc:`SHA-512 Crypt `. + * :doc:`passlib.hash.pbkdf2_{digest} ` -- + for some other PBKDF2-based hashes. Interface ========= @@ -33,8 +30,8 @@ Format & Algorithm ================== -All of this scheme's hashes have the format :``{PKCS5S2}``, -where :samp:`` is a 64 character base64 encoded string; +All of this scheme's hashes have the format :samp:`\\{PKCS5S2\\}{data}`, +where :samp:`{data}` is a 64 character base64 encoded string; which (when decoded), contains a 16 byte salt, and a 32 byte checksum. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.bcrypt.rst passlib-1.6.1/docs/lib/passlib.hash.bcrypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.bcrypt.rst 2011-10-08 04:49:28.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.bcrypt.rst 2012-08-02 18:54:05.000000000 +0000 @@ -9,60 +9,87 @@ a large salt and variable number of rounds, it's currently the default password hash for many systems (notably BSD), and has no known weaknesses. It is one of the three hashes Passlib :ref:`recommends ` -for new applications. - -.. note:: - - It is strongly recommended to install - :ref:`PyBcrypt or BCryptor ` - if this algorithm is going to be used. - -Usage -===== -This class can be used directly as follows:: +for new applications. This class can be used directly as follows:: >>> from passlib.hash import bcrypt - >>> #generate new salt, encrypt password + >>> # generate new salt, encrypt password >>> h = bcrypt.encrypt("password") >>> h '$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy' - >>> #same, but with explict number of rounds + >>> # the same, but with an explicit number of rounds >>> bcrypt.encrypt("password", rounds=8) '$2a$08$8wmNsdCH.M21f.LSBSnYjQrZ9l1EmtBc9uNPGL.9l75YE8D8FlnZC' - >>> #check if hash is a bcrypt hash - >>> bcrypt.identify(h) - True - >>> #check if some other hash is recognized - >>> bcrypt.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') - False - - >>> #verify correct password + >>> # verify password >>> bcrypt.verify("password", h) True - >>> #verify incorrect password >>> bcrypt.verify("wrong", h) False +.. note:: + + It is strongly recommended that you install + `py-bcrypt `_ + when using this hash. + +.. seealso:: the generic :ref:`PasswordHash usage examples ` + Interface ========= -.. autoclass:: bcrypt +.. autoclass:: bcrypt() + +.. _bcrypt-backends: + +.. index:: + pair: environmental variable; PASSLIB_BUILTIN_BCRYPT + +.. note:: + + This class will use the first available of four possible backends: + + 1. `py-bcrypt `_, if installed. + 2. `bcryptor `_, if installed. + 3. stdlib's :func:`crypt.crypt()`, if the host OS supports BCrypt + (primarily BSD-derived systems). + 4. A pure-python implementation of BCrypt, built into Passlib. + + If no backends are available, :meth:`encrypt` and :meth:`verify` + will throw :exc:`~passlib.exc.MissingBackendError` when they are invoked. + You can check which backend is in use by calling :meth:`!bcrypt.get_backend()`. + +.. warning:: + The pure-python backend (#4) is disabled by default! + + That backend is currently too slow to be usuable given the number of rounds required + for security. That said, if you have no other alternative and need to use it, + set the environmental variable ``PASSLIB_BUILTIN_BCRYPT="enabled"`` + before importing Passlib. + + What's "too slow"? Passlib's :ref:`rounds selection guidelines ` + currently require BCrypt be able to do >= 12 cost in <= 300ms. By this standard + the pure-python backend is 128x too slow under CPython 2.7, and 16x too slow under PyPy 1.8. + (speedups are welcome!) Format & Algorithm ================== Bcrypt is compatible with the :ref:`modular-crypt-format`, and uses ``$2$`` and ``$2a$`` as the identifying prefix for all it's strings (``$2$`` is seen only for legacy hashes which used an older version of Bcrypt). -An example hash (of ``password``) is ``$2a$12$GhvMmNVjRW29ulnudl.LbuAnUtN/LRfe1JsBm1Xu6LE3059z5Tr8m``. +An example hash (of ``password``) is: + + ``$2a$12$GhvMmNVjRW29ulnudl.LbuAnUtN/LRfe1JsBm1Xu6LE3059z5Tr8m`` + Bcrypt hashes have the format :samp:`$2a${rounds}${salt}{checksum}`, where: -* :samp:`{rounds}` is the cost parameter, encoded as 2 zero-padded decimal digits, +* :samp:`{rounds}` is a cost parameter, encoded as 2 zero-padded decimal digits, which determines the number of iterations used via :samp:`{iterations}=2**{rounds}` (rounds is 12 in the example). -* :samp:`{salt}` is the 22 character salt string, using the characters in the regexp range ``[./A-Za-z0-9]`` (``GhvMmNVjRW29ulnudl.Lbu`` in the example). -* :samp:`{checksum}` is the 31 character checksum, using the same characters as the salt (``AnUtN/LRfe1JsBm1Xu6LE3059z5Tr8m`` in the example). +* :samp:`{salt}` is a 22 character salt string, using the characters in the regexp range ``[./A-Za-z0-9]`` (``GhvMmNVjRW29ulnudl.Lbu`` in the example). +* :samp:`{checksum}` is a 31 character checksum, using the same characters as the salt (``AnUtN/LRfe1JsBm1Xu6LE3059z5Tr8m`` in the example). -BCrypt's algorithm is described in detail in it's specification document [#f1]_. +While BCrypt's basic algorithm is described in it's design document [#f1]_, +the OpenBSD implementation [#f2]_ is considered the canonical reference, even +though it differs from the design document in a few small ways. Deviations ========== @@ -72,8 +99,8 @@ BCrypt does not specify what the behavior should be when passed a salt string outside of the regexp range ``[./A-Za-z0-9]``. - In order to avoid this situtation, PassLib strictly limits salts to the - allowed character set, and will throw a ValueError if an invalid + In order to avoid this situtation, Passlib strictly limits salts to the + allowed character set, and will throw a :exc:`ValueError` if an invalid salt character is encountered. * Unicode Policy: @@ -85,10 +112,10 @@ as well as all known reference hashes. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through bcrypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. * Padding Bits @@ -96,13 +123,44 @@ encoding only 2 bits of data, the remaining 4 are "padding" bits. Similarly, the last character of the digest contains 4 bits of data, and 2 padding bits. Because of the way they are coded, many BCrypt implementations - will reject all passwords if these padding bits are not set to 0. - Due to a legacy issue with Passlib <= 1.5.2, - Passlib instead prints a warning if it encounters hashes with any padding bits set, - and will then validate them correctly. + will reject *all* passwords if these padding bits are not set to 0. + Due to a legacy :ref:`issue ` with Passlib <= 1.5.2, + Passlib will print a warning if it encounters hashes with any padding bits set, + and then validate the hash as if the padding bits were cleared. (This behavior will eventually be deprecated and such hashes will throw a :exc:`ValueError` instead). +* The *crypt_blowfish* 8-bit bug + + .. _crypt-blowfish-bug: + + Pre-1.1 versions of the `crypt_blowfish `_ + bcrypt implementation suffered from a serious flaw [#eight]_ + in how they handled 8-bit passwords. The manner in which the flaw was fixed resulted + in *crypt_blowfish* adding support for two new BCrypt hash identifiers: + + ``$2x$``, allowing sysadmins to mark any ``$2a$`` hashes which were potentially + generated with the buggy algorithm. Passlib 1.6 recognizes (but does not + currently support generating or verifying) these hashes. + + ``$2y$``, the default for crypt_blowfish 1.1 and newer, indicates + the hash was generated with the canonical OpenBSD-compatible algorithm, + and should match *correctly* generated ``$2a$`` hashes. + Passlib 1.6 can generate and verify these hashes. + + As well, crypt_blowfish 1.2 modified the way it generates ``$2a$`` hashes, + so that passwords containing the byte value 0xFF are hashed in a manner + incompatible with either the buggy or canonical algorithms. Passlib + does not support this algorithmic variant either, though it should + be *very* rarely encountered in practice. + .. rubric:: Footnotes -.. [#f1] ``_ - the bcrypt format specification +.. [#f1] the bcrypt format specification - + ``_ + +.. [#f2] the OpenBSD BCrypt source - + ``_ + +.. [#eight] The flaw in pre-1.1 crypt_blowfish is described here - + `CVE-2011-2483 `_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.bigcrypt.rst passlib-1.6.1/docs/lib/passlib.hash.bigcrypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.bigcrypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.bigcrypt.rst 2012-05-03 16:36:58.000000000 +0000 @@ -2,41 +2,54 @@ :class:`passlib.hash.bigcrypt` - BigCrypt ======================================================================= -.. currentmodule:: passlib.hash - -This class implements BigCrypt (a modified version of des-crypt) commonly -found on HP-UX, Digital Unix, and OSF/1. The main difference with -:class:`~passlib.hash.des_crypt` is that bigcrypt -uses all the characters of a password, not just the first 8, -and has a variable length hash string. - .. warning:: This algorithm is dangerously weak, and should not be used if at all possible. -Usage -===== -This class can be used in exactly the same manner as :class:`~passlib.hash.des_crypt`. +.. currentmodule:: passlib.hash + +This class implements BigCrypt (a modified version of DES-Crypt) commonly +found on HP-UX, Digital Unix, and OSF/1. The main difference between it and +:class:`~passlib.hash.des_crypt` is that BigCrypt +uses all the characters of a password, not just the first 8, +and has a variable length hash. + +.. seealso:: + :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. Interface ========= -.. autoclass:: bigcrypt(checksum=None, salt=None, strict=False) +.. autoclass:: bigcrypt() Format ====== An example hash (of the string ``passphrase``) is ``S/8NbAAlzbYO66hAa9XZyWy2``. A bigcrypt hash string has the format :samp:`{salt}{checksum_1}{checksum_2...}{checksum_n}` for some integer :samp:`{n}>0`, where: -* :samp:`{salt}` is the salt, stored as a 2 character :func:`hash64 `-encoded +* :samp:`{salt}` is the salt, stored as a 2 character :data:`hash64 `-encoded 12-bit integer (``S/`` in the example). * each :samp:`{checksum_i}` is a separate checksum, stored as an 11 character - :func:`hash64 `-encoded 64-bit integer (``8NbAAlzbYO6`` and ``6hAa9XZyWy2`` + :data:`hash64-big `-encoded 64-bit integer (``8NbAAlzbYO6`` and ``6hAa9XZyWy2`` in the example). * the integer :samp:`n` (the number of checksums) is determined by the formula :samp:`{n}=min(1, (len({secret})+7)//8)`. +.. note:: + + This hash format lacks any magic prefix that can be used to unambiguously + identify it. Out of context, certain :class:`!bigcrypt` hashes may + be confused with that of two other algorithms: + + * :class:`des_crypt` - BigCrypt hashes of passwords with < 8 characters + are exactly the same as the Des-Crypt hash of the same password. + + * :class:`crypt16` - BigCrypt hashes of passwords with + 9 to 16 characters have the same size and character set as + Crypt-16 hashes; though the actual algorithms are different. + .. rst-class:: html-toggle Algorithm @@ -54,7 +67,7 @@ 4. The 2 character salt string is decoded to a 12-bit integer salt value; The salt string uses little-endian - :func:`hash64 ` encoding. + :data:`hash64 ` encoding. 5. 25 repeated rounds of modified DES encryption are performed; starting with a null input block, @@ -68,7 +81,7 @@ lsb-padded with 2 zero bits. 7. The resulting 66-bit integer is encoded in big-endian order - using the :func:`hash 64 ` format. + using the :data:`hash64-big ` format. This forms the first checksum segment. 8. For each additional block of 8 bytes in the padded password (from step 2), @@ -95,7 +108,7 @@ * It suffers from all the flaws of :class:`~passlib.hash.des_crypt`. -* Since checksum in it's hash is essentially a separate +* Since each checksum compontent in it's hash is essentially a separate des-crypt checksum, they can be attacked in parallel. * It reveals information about the length of the encoded @@ -117,8 +130,8 @@ various limits on maximum password length (commonly, 128 chars), and discard the remaining part of the password. - Thus, while PassLib should be able to verify all existing - bigcrypt hashes, other systems may require hashes generated by PassLib + Thus, while Passlib should be able to verify all existing + bigcrypt hashes, other systems may require hashes generated by Passlib to be truncated to their specific maximum length. * Unicode Policy: @@ -127,10 +140,10 @@ (as evidenced by the fact that it discards the 8th bit of all password bytes). In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through bigcrypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.bsdi_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.bsdi_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.bsdi_crypt.rst 2011-10-08 04:49:28.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.bsdi_crypt.rst 2012-05-03 16:36:58.000000000 +0000 @@ -4,38 +4,48 @@ .. currentmodule:: passlib.hash +.. note:: + + This algorithm is weak by modern standards, and should not be used in new applications. + This algorithm was developed by BSDi for their BSD/OS distribution. It's based on :class:`~passlib.hash.des_crypt`, and contains a larger salt and a variable number of rounds. This algorithm is also known as "Extended DES Crypt". -This algorithm is weak by modern standards, -and should not be used in new applications. - -Usage -===== -This class can be used directly as follows:: +It class can be used directly as follows:: - >>> from passlib.hash import bsdi_crypt as bc + >>> from passlib.hash import bsdi_crypt - >>> bc.encrypt("password") #generate new salt, encrypt password - '_cD..Bf/46u7tr9IAJ6M' + >>> # generate new salt, encrypt password + >>> hash = bsdi_crypt.encrypt("password") + >>> hash + '_7C/.Bf/4gZk10RYRs4Y' - >>> bc.encrypt("password", rounds=10000) #same, but with explict number of rounds - '_EQ0.amG/Pp5b0hIpggo' + >>> # same, but with explict number of rounds + >>> bsdi_crypt.encrypt("password", rounds=10001) + '_FQ0.amG/zwCMip7DnBk' - >>> bc.identify('_cD..Bf/46u7tr9IAJ6M') #check if hash is recognized + >>> # verify password + >>> bsdi_crypt.verify("password", hash) True - >>> bc.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if some other hash is recognized + >>> bsdi_crypt.verify("secret", hash) False - >>> bc.verify("password", '_cD..Bf/46u7tr9IAJ6M') #verify correct password - True - >>> bc.verify("secret", '_cD..Bf/46u7tr9IAJ6M') #verify incorrect password - False +.. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= -.. autoclass:: bsdi_crypt(checksum=None, salt=None, rounds=None, strict=False) +.. autoclass:: bsdi_crypt() + +.. note:: + + This class will use the first available of two possible backends: + + * stdlib :func:`crypt()`, if the host OS supports BSDi-Crypt + (primarily BSD-derived systems). + * a pure Python implementation of BSDi-Crypt built into Passlib. + + You can see which backend is in use by calling the :meth:`get_backend()` method. Format ====== @@ -44,7 +54,7 @@ All characters except the underscore prefix are drawn from ``[./0-9A-Za-z]``. * ``_`` - the underscore is used to distinguish this scheme from others, such as des-crypt. -* :samp:`{rounds}` is the number of rounds, stored as a 4 character :mod:`hash64 `-encoded 24-bit integer (``EQ0.`` in the example). +* :samp:`{rounds}` is the number of rounds, stored as a 4 character :data:`hash64 `-encoded 24-bit integer (``EQ0.`` in the example). * :samp:`{salt}` is the salt, stored as as a 4 character hash64-encoded 24-bit integer (``jzhS`` in the example). * :samp:`{checksum}` is the checksum, stored as an 11 character hash64-encoded 64-bit integer (``VeUyoSqLupI`` in the example). @@ -60,12 +70,12 @@ 1. Given a password string, a salt string, and rounds string. 2. The 4 character rounds string is decoded to a 24-bit integer rounds value; - The rounds string uses little-endian - :func:`hash64 ` encoding. + The rounds string uses little-endian :data:`hash64 ` + encoding. 3. The 4 character salt string is decoded to a 24-bit integer salt value; - The salt string uses little-endian - :func:`hash64 ` encoding. + The salt string uses little-endian :data:`hash64 ` + encoding. 4. The password is NULL-padded on the end to the smallest non-zero multiple of 8 bytes. @@ -95,7 +105,9 @@ lsb-padded with 2 zero bits. 9. The resulting 66-bit integer is encoded in big-endian order - using the :func:`hash 64 ` format. + using the :data:`hash64-big ` format. + +.. _bsdi-crypt-security-issues: Security Issues =============== @@ -110,11 +122,16 @@ * The fact that it only uses the lower 7 bits of each byte of the password restricts the keyspace which needs to be searched. -.. note:: - - This algorithm is none-the-less stronger than des-crypt itself, - since it supports variable rounds, a larger salt size, - and uses all bytes of the password. +* Additionally, even *rounds* values are slightly weaker still, + as they may reveal the hash used one of the weak DES keys [#weak]_. + This information could theoretically allow an attacker to perform a + brute-force attack on a reduced keyspace and against only 1-2 rounds of DES. + (This issue is mitagated by the fact that few passwords are both valid *and* + result in a weak key). + +This algorithm is none-the-less stronger than :class:`!des_crypt` itself, +since it supports variable rounds, a larger salt size, +and uses all the bytes of the password. Deviations ========== @@ -126,10 +143,10 @@ (as evidenced by the fact that it discards the 8th bit of all password bytes). In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through bsdi-crypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes @@ -138,3 +155,5 @@ .. [#] Another source describing algorithm - ``_ + +.. [#weak] DES weak keys - ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.cisco_pix.rst passlib-1.6.1/docs/lib/passlib.hash.cisco_pix.rst --- passlib-1.5.3/docs/lib/passlib.hash.cisco_pix.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.cisco_pix.rst 2012-08-01 17:05:50.000000000 +0000 @@ -0,0 +1,139 @@ +.. index:: Cisco; PIX hash + +================================================================== +:class:`passlib.hash.cisco_pix` - Cisco PIX hash +================================================================== + +.. versionadded:: 1.6 + +.. warning:: + + This hash is not secure, and should not be used for any purposes + besides manipulating existing Cisco PIX password hashes. + +.. currentmodule:: passlib.hash + +This class implements the password hash algorithm commonly found on Cisco +PIX firewalls. This class can be used directly as follows:: + + >>> from passlib.hash import cisco_pix as pix + + >>> # encrypt password using specified username + >>> hash = pix.encrypt("password", user="user") + >>> hash + 'A5XOy94YKDPXCo7U' + + >>> # verify correct password + >>> pix.verify("password", hash, user="user") + True + >>> # verify correct password w/ wrong username + >>> pm.verify("password", hash, user="other") + False + >>> # verify incorrect password + >>> pm.verify("letmein", hash, user="user") + False + + >>> # encrypt password without associate user account + >>> hash2 = pix.encrypt("password") + >>> hash2 + 'NuLKvvWGg.x9HEKO' + + >>> # verify password without associated user account + >>> pix.verify("password", hash2) + True + +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +Interface +========= +.. autoclass:: cisco_pix() + +.. note:: + + This hash algorithm has a context-sensitive percularity. + It takes in an optional username, used to salt the hash, + but with specific restrictions... + + * The username *must* be provided in order to correctly hash passwords + associated with a user account on the Cisco device. + + * Conversely, the username *must not* be provided (or must be set to ``""``) + in order to correctly hash passwords which don't have an associated user + account (such as the "enable" password). + +.. rst-class:: html-toggle + +Format & Algorithm +================== +Cisco PIX hashes consist of a 12 byte digest, encoded as a 16 character +:data:`HASH64 `-encoded string. An example +hash (of ``"password"``) is ``"NuLKvvWGg.x9HEKO"``. + +The digest is calculated as follows: + +1. The password is encoded using an ``ASCII``-compatible encoding + (all known references are strict 7-bit ascii, and Passlib uses ``UTF-8`` + to provide unicode support). +2. If the hash is associated with a user account, + append the first four bytes of the user account name + to the end of the password. If the hash is NOT associated + with a user account (e.g. it's the "enable" password), + this step should be omitted. +3. The resulting password should be truncated to 16 bytes, + or the right side NULL padded to 16 bytes, as appropriate. +4. Run the result of step 3 through MD5. +5. Discard every 4th byte of the 16-byte MD5 hash, starting + with the 4th byte. +6. Encode the 12-byte result using :data:`HASH64 `. + +Security Issues +=============== +This algorithm is not suitable for *any* use besides manipulating existing +Cisco PIX hashes, due to the following flaws: + +* It's use of the username as a salt value (and only the first four characters + at that), means that common usernames (e.g. ``admin``, ``cisco``) will occur + more frequently as salts, weakening the effectiveness of the salt in + foiling pre-computed tables. + +* It's truncation of the ``password+user`` combination to 16 characters + additionally limits the keyspace, and the effectiveness of the username + as a salt; making pre-computed and brute force attacks much more feasible. + +* Since the keyspace of ``user+password`` is still a subset of ascii characters, + existing MD5 lookup tables have an increased chance of being able to + reverse common hashes. + +* It's simplicity, and the weakness of MD5, makes high-speed brute force attacks + much more feasible. + +* Furthermore, it discards of 1/4 of MD5's already small 16 byte digest, + making collisions much more likely. + +Deviations +========== +This implementation differs from the standard in one main way: + +* Unicode Policy: + + The official Cisco PIX algorithm is primarily used with ``ascii`` passwords, + how it handles other characters is not known. + + In order to provide support for unicode strings, + Passlib will encode unicode passwords using ``utf-8`` + before running them through this algorithm. If a different + encoding is desired by an application, the password should be encoded + before handing it to Passlib. + +* While this implementation agrees with all known references, + the actual algorithm has not been published by Cisco, so there may be other + unknown deviations. + +.. rubric:: Footnotes + +.. [#] Description of PIX algorithm - + ``_ + +.. [#] Message threads hinting at how username is handled - + ``_, + ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.cisco_type7.rst passlib-1.6.1/docs/lib/passlib.hash.cisco_type7.rst --- passlib-1.5.3/docs/lib/passlib.hash.cisco_type7.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.cisco_type7.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,129 @@ +.. index:: Cisco; Type 7 hash + +================================================================== +:class:`passlib.hash.cisco_type7` - Cisco "Type 7" hash +================================================================== + +.. versionadded:: 1.6 + +.. warning:: + + This is not a hash, this is a reversible plaintext encoding. + **This format can be trivially decoded**. + +.. currentmodule:: passlib.hash + +This class implements the "Type 7" password encoding used Cisco IOS. +This is not actually a true hash, but a reversible XOR Cipher encoding the plaintext +password. Type 7 strings are (and were designed to be) plaintext equivalent; +the goal was to protect from "over the shoulder" eavesdropping, and +little else. They can be trivially decoded. +This class can be used directly as follows:: + + >>> from passlib.hash import cisco_type7 + + >>> # encode password + >>> h = cisco_type7.encrypt("password") + >>> h + '044B0A151C36435C0D' + + >>> # verify password + >>> cisco_type7.verify("password", h) + True + >>> pm.verify("letmein", h) + False + + >>> # to demonstrate this is an encoding, not a real hash, + >>> # this class supports decoding the resulting string: + >>> cisco_type7.decode(h) + "password" + +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +.. note:: + + This implementation should work correctly for most cases, but may not + fully implement some edge cases (see `Deviations`_ below). + Please report any issues encountered. + +Interface +========= +.. autoclass:: cisco_type7() + +.. rst-class:: html-toggle + +Format & Algorithm +================== +The Cisco Type 7 encoding consists of two decimal digits +(encoding the salt), followed a series of hexdecimal characters, +two for every byte in the encoded password. +An example encoding (of ``"password"``) is ``044B0A151C36435C0D``. +This has a salt/offset of 4 (``04`` in the example), +and encodes password via ``4B0A151C36435C0D``. + +.. note:: + The following description may not be entirely correct with + respect to the official algorithm, see the `Deviations`_ section for details. + +The algorithm is a straightforward XOR Cipher: + +1. The algorithm relies on the following ``ascii``-encoded 53-byte + constant:: + + "dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87" + +2. A integer salt should be generated from the range + 0 .. 15. The first two characters of the encoded string are the + zero-padded decimal encoding of the salt. + +3. The remaining characters of the encoded string are generated as follows: + For each byte in the password (starting with the 0th byte), + the :samp:`{i}`'th byte of the password is encoded as follows: + + a. let ``j=(i + salt) % 53`` + b. XOR the :samp:`{i}`'th byte of the password with the :samp:`{j}`'th byte + of the magic constant. + c. encode the resulting byte as uppercase hexidecimal, + and append to the encoded string. + +Deviations +========== +This implementation differs from the official one in a few ways. +It may be updated as more information becomes available. + +* Unicode Policy: + + Type 7 encoding is primarily used with ``ASCII`` passwords, + how it handles other characters is not known. + + In order to provide support for unicode strings, Passlib will encode unicode + passwords using ``UTF-8`` before running them through this algorithm. If a + different encoding is desired by an application, the password should be + encoded before handing it to Passlib. + +* Magic Constant: + + Other implementations contain a truncated 26-byte constant instead of the + 53-byte constant listed above. However, it is likely those implementations + were merely incomplete, as they exhibit other issues as well after + the 26th byte is reached (throwing an error, truncating the password, + outputing garbage), and only worked for shorter passwords. + +* Salt Range: + + All known test vectors contain salt values in ``range(0,16)``. + However, the algorithm itself should be able to handle any salt value + in ``range(0,53)`` (the size of the key). For maximum compatibility with + other implementations, Passlib will accept ``range(0,53)``, but only + generate salts in ``range(0,16)``. + +* While this implementation handles all known test vectors, + and tries to make sense of the disparate implementations, + the actual algorithm has not been published by Cisco, + so there may be other unknown deviations. + +.. rubric:: Footnotes + +.. [#] Description of Type 7 algorithm - + ``_, + ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.crypt16.rst passlib-1.6.1/docs/lib/passlib.hash.crypt16.rst --- passlib-1.5.3/docs/lib/passlib.hash.crypt16.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.crypt16.rst 2012-05-03 16:36:58.000000000 +0000 @@ -2,42 +2,43 @@ :class:`passlib.hash.crypt16` - Crypt16 ======================================================================= +.. warning:: + + This algorithm is dangerously weak, and should not be used if at all possible. + .. currentmodule:: passlib.hash This class implements the Crypt16 password hash, commonly found on Ultrix and Tru64. It's a minor modification of :class:`~passlib.hash.des_crypt`, which allows passwords of up to 16 characters. -.. warning:: - - This algorithm is dangerously weak, and should not be used if at all possible. - -.. note:: - - This format is frequently confused with :class:`~passlib.hash.bigcrypt`, - another derivative of des-crypt, because (for passwords between - 9 and 16 chars in length) bigcrypt hashes will have - the same size and character set. - -Usage -===== -This class can be used in exactly the same manner as :class:`~passlib.hash.des_crypt`. +.. seealso:: + :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. Interface ========= -.. autoclass:: crypt16(checksum=None, salt=None, strict=False) +.. autoclass:: crypt16() Format ====== An example hash (of the string ``passphrase``) is ``aaX/UmCcBrceQ0kQGGWKTbuE``. A crypt16 hash string has the format :samp:`{salt}{checksum_1}{checksum_2}`, where: -* :samp:`{salt}` is the salt, stored as a 2 character :func:`hash64 `-encoded - 12-bit integer (``aa`` in the example). +* :samp:`{salt}` is the salt, stored as a 2 character + :data:`hash64 `-encoded 12-bit integer (``aa`` in the + example). * each :samp:`{checksum_i}` is a separate checksum, stored as an 11 character - :func:`hash64 `-encoded 64-bit integer (``X/UmCcBrceQ`` and ``0kQGGWKTbuE`` - in the example). + :data:`hash64-big `-encoded 64-bit integer + (``X/UmCcBrceQ`` and ``0kQGGWKTbuE`` in the example). + +.. note:: + + This hash is frequently confused with the :doc:`bigcrypt ` + hash algorithm, as it has the same size and uses the same character + set as a :class:`!bigcrypt` hash of a password with 9 to 16 + characters; though the actual algorithms are different. .. rst-class:: html-toggle @@ -48,10 +49,11 @@ 1. Given a password string and a salt string. 2. The 2 character salt string is decoded to a 12-bit integer salt value; - The salt string uses little-endian - :func:`hash64 ` encoding. + The salt string uses little-endian :data:`hash64 ` + encoding. -3. The password is NULL padded at the end or truncated to 16 bytes, as appropriate. +3. If the password is larger than 16 bytes, the end is truncated to 16 bytes. + If the password is smaller than 16 bytes, the end is NULL padded to 16 bytes. 4. The lower 7 bits of the first 8 characters of the password are used to form a 56-bit integer; with the first character providing @@ -62,15 +64,16 @@ starting with a null input block, and using the 56-bit integer from step 4 as the DES key. - The salt is used to to mutate the normal DES encrypt operation - by swapping bits :samp:`{i}` and :samp:`{i}+24` in the DES E-Box output - if and only if bit :samp:`{i}` is set in the salt value. + The salt value from step 2 is used to to mutate the normal + DES encrypt operation by swapping bits :samp:`{i}` and :samp:`{i}+24` + in the DES E-Box output if and only if bit :samp:`{i}` is set in + the salt value. 6. The 64-bit result of the last round of step 5 is then lsb-padded with 2 zero bits. 7. The resulting 66-bit integer is encoded in big-endian order - using the :func:`hash 64 ` format. + using the :data:`hash64-big ` format. This is the first checksum segment. 8. The second checksum segment is created by repeating @@ -107,10 +110,10 @@ (as evidenced by the fact that it discards the 8th bit of all password bytes). In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through crypt16. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.cta_pbkdf2_sha1.rst passlib-1.6.1/docs/lib/passlib.hash.cta_pbkdf2_sha1.rst --- passlib-1.5.3/docs/lib/passlib.hash.cta_pbkdf2_sha1.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.cta_pbkdf2_sha1.rst 2012-05-03 16:36:58.000000000 +0000 @@ -2,7 +2,7 @@ :class:`passlib.hash.cta_pbkdf2_sha1` - Cryptacular's PBKDF2 hash ================================================================= -.. index:: pbkdf2 hash; cryptacular +.. index:: pbkdf2 hash; Cryptacular .. currentmodule:: passlib.hash @@ -13,24 +13,16 @@ .. seealso:: - * :doc:`passlib.hash.pbkdf2_digest ` - for some other PBKDF2-based hashes. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. - * :doc:`passlib.hash.dlitz_pbkdf2_sha1 ` + * :doc:`dlitz_pbkdf2_sha1 ` for another hash which looks almost exactly like this one. -Usage -===== -This class support both rounds and salts, -and can be used in the exact same manner -as :doc:`SHA-512 Crypt `. - Interface ========= .. autoclass:: cta_pbkdf2_sha1() -.. rst-class:: html-toggle - Format & Algorithm ================== diff -Nru passlib-1.5.3/docs/lib/passlib.hash.des_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.des_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.des_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.des_crypt.rst 2012-05-03 16:36:58.000000000 +0000 @@ -2,44 +2,49 @@ :class:`passlib.hash.des_crypt` - DES Crypt ======================================================================= -.. currentmodule:: passlib.hash - -This class implements the original DES-based Unix Crypt algorithm. -While no longer in active use in most places, -it is supported for legacy purposes by many Unix flavors. - .. warning:: This algorithm is extremely weak by modern standards, and should not be used if possible. -Usage -===== -This class can be used directly as follows:: +.. currentmodule:: passlib.hash - >>> from passlib.hash import des_crypt as dc +This class implements the original DES-based Unix Crypt algorithm. +While no longer in active use in most places, +it is supported for legacy purposes by many Unix flavors. +It can used directly as follows:: + + >>> from passlib.hash import des_crypt - >>> dc.encrypt("password") #generate new salt, encrypt password + >>> # generate new salt, encrypt password + >>> hash = des_crypt.encrypt("password") 'JQMuyS6H.AGMo' - >>> dc.identify('JQMuyS6H.AGMo') #check if hash is recognized + >>> # verify the password + >>> des_crypt.verify("password", hash) True - >>> dc.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if some other hash is recognized + >>> des_crypt.verify("letmein", hash) False - >>> dc.verify("password", 'JQMuyS6H.AGMo') #verify correct password - True - >>> dc.verify("secret", 'JQMuyS6H.AGMo') #verify incorrect password - False +.. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= -.. autoclass:: des_crypt(checksum=None, salt=None, strict=False) +.. autoclass:: des_crypt() + +.. note:: + + This class will use the first available of two possible backends: + + * stdlib :func:`crypt()`, if the host OS supports DES-Crypt (most Unix systems). + * a pure Python implementation of DES-Crypt built into Passlib. + + You can see which backend is in use by calling the :meth:`get_backend()` method. Format ====== A des-crypt hash string consists of 13 characters, drawn from ``[./0-9A-Za-z]``. -The first 2 characters form a :mod:`hash64 `-encoded +The first 2 characters form a :data:`hash64 `-encoded 12 bit integer used as the salt, with the remaining characters forming a hash64-encoded 64-bit integer checksum. @@ -57,8 +62,8 @@ 1. Given a password string and a salt string. 2. The 2 character salt string is decoded to a 12-bit integer salt value; - The salt string uses little-endian - :func:`hash64 ` encoding. + The salt string uses little-endian :data:`hash64 ` + encoding. 3. If the password is less than 8 bytes, it's NULL padded at the end to 8 bytes. @@ -83,8 +88,8 @@ 6. The 64-bit result of the last round of step 5 is then lsb-padded with 2 zero bits. -7. The resulting 66-bit integer is encoded in big-endian order - using the :func:`hash 64 ` format. +7. The resulting 66-bit integer is encoded in big-endian order using the + :data:`hash64-big ` format. Security Issues =============== @@ -109,17 +114,17 @@ However, behavior in these cases varies wildly; with implementations returning everything from errors to incorrect hashes that never validate. - To avoid all this, PassLib will throw an "invalid salt" if the provided + To avoid all this, Passlib will throw an "invalid salt" if the provided salt string is not at least 2 characters. * Restricted salt string character set: The underlying algorithm expects salt strings to use the - :mod:`hash64 ` character set to encode + :data:`hash64 ` character set to encode a 12-bit integer. Many implementations of des-crypt will accept a salt containing other characters, but vary wildly in how they are handled, including errors and implementation-specific value mappings. - To avoid all this, PassLib will throw an "invalid salt" if the salt + To avoid all this, Passlib will throw an "invalid salt" if the salt string contains any non-standard characters. * Unicode Policy: @@ -128,11 +133,12 @@ (as evidenced by the fact that it discards the 8th bit of all password bytes). In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through des-crypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes -.. [#] A java implementation of des-crypt, used as base for PassLib's pure-python implementation, is located at ``_ +.. [#] A java implementation of des-crypt, used as base for Passlib's pure-python implementation, + can be found at ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.django_std.rst passlib-1.6.1/docs/lib/passlib.hash.django_std.rst --- passlib-1.5.3/docs/lib/passlib.hash.django_std.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.django_std.rst 2012-05-03 16:36:58.000000000 +0000 @@ -1,4 +1,4 @@ -.. index:: django; hash formats +.. index:: Django; hash formats ============================================================= :samp:`passlib.hash.django_{digest}` - Django-specific Hashes @@ -12,55 +12,126 @@ This module's password hashing code supports a few simple salted digests, stored using the format :samp:`{id}${salt}${checksum}` (where :samp:`{id}` is an identifier assigned by Django). -Passlib provides support for all the hashes used by Django: - -* :class:`django_salted_sha1` - simple salted SHA1 digest, currently Django's default. -* :class:`django_salted_md5` - simple salted MD5 digest. -* :class:`django_des_crypt` - support for legacy :class:`des_crypt` hashes, - shoehorned into Django's hash format. -* :class:`hex_md5` - historical format used by old Django versions. - -.. warning:: - - All of these hashes are suceptible to brute-force attacks; - even the strongest of these (:class:`django_salted_sha1`) - is a simple single-round salted digest. - They should not be used for any purpose - besides manipulating existing Django password hashes. +Passlib provides support for all the hashes used up to and including +Django 1.4 .. seealso:: - * :data:`passlib.apps.django_context` - a premade Django context - which can read all of the formats listed below. + * :ref:`passlib.apps.django_context ` - + a set of premade contexts which mimic Django's builtin hashing policy, + and can read all of the formats listed below. +.. * :mod:`passlib.ext.django` - a plugin that updates Django to use a stronger hashing scheme, and migrates existing hashes as users log in. -Salted Hashes -============= +.. _django-1.4-hashes: + +Django 1.4 Hashes +================= +Django 1.4 introduced a new "hashers" framework, as well as +three new modern large-salt variable-cost hash algorithms: + +* :class:`django_pbkdf2_sha256` - a PBKDF2-HMAC-SHA256 based hash. +* :class:`django_pbkdf2_sha1` - a PBKDF2-HMAC-SHA1 based hash. +* :class:`django_bcrypt` - a wrapper around :class:`~passlib.hash.bcrypt`. -Usage ------ These classes can be used directly as follows:: - >>> from passlib.hash import django_salted_sha1 as dss + >>> from passlib.hash import django_pbkdf2_sha256 as handler - >>> #encrypt password - >>> h = dss.encrypt("password") + >>> # encrypt password + >>> h = handler.encrypt("password") >>> h - 'sha1$c6218$161d1ac8ab38979c5a31cbaba4a67378e7e60845' + 'pbkdf2_sha256$10000$s1w0UXDd00XB$+4ORmyvVWAQvoAEWlDgN34vlaJx1ZTZpa1pCSRey2Yk=' - >>> lms.identify(h) #check if hash is recognized + >>> # verify password + >>> handler.verify("password", h) True - >>> lms.identify('JQMuyS6H.AGMo') #check if some other hash is recognized + >>> handler.verify("eville", h) False - >>> lms.verify("password", h) #verify correct password +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +Interface +--------- + +.. autoclass:: django_pbkdf2_sha256() +.. autoclass:: django_pbkdf2_sha1() +.. data:: django_bcrypt() + + This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`. + + This is identical to :class:`!bcrypt` itself, but with + the Django-specific prefix ``"bcrypt$"`` prepended. + See :doc:`/lib/passlib.hash.bcrypt` for more details, + the usage and behavior is identical. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!BCryptPasswordHasher` class. + + .. versionadded:: 1.6 + +Format +------ +An example :class:`!django_pbkdf2_sha256` hash (of ``password``) is: + + ``pbkdf2_sha256$10000$s1w0UXDd00XB$+4ORmyvVWAQvoAEWlDgN34vlaJx1ZTZpa1pCSRey2Yk=`` + +Both of Django's PBKDF2 hashes have the same basic format, +:samp:`{ident}${rounds}${salt}${checksum}`, where: + +* :samp:`{ident}` is an identifier (``pbkdf2_sha256`` in the case of the example). + +* :samp:`{rounds}` is a variable cost parameter encoded in decimal. + +* :samp:`{salt}` consists of (usually 12) alphanumeric digits + (``s1w0UXDd00XB`` in the example). + +* :samp:`{checksum}` is the base64 encoding the PBKDF2 digest. + +The digest porition is generated by passing the ``utf-8`` encoded password, +the ``ascii``-encoded salt string, and the number of rounds into +PBKDF2 using the HMAC-SHA256 prf; and generated a 32 byte checksum, +which is then encoding using base64. + +The other PBKDF2 wrapper functions similarly. + +Django 1.0 Hashes +================= +.. warning:: + + All of the following hashes are very susceptible to brute-force attacks; + since they are simple single-round salted digests. + They should not be used for any purpose + besides manipulating existing Django password hashes. + +Django 1.0 supports some basic salted digests, as well as some +legacy hashes: + +* :class:`django_salted_sha1` - simple salted SHA1 digest, Django 1.0-1.3's default. +* :class:`django_salted_md5` - simple salted MD5 digest. +* :class:`django_des_crypt` - support for legacy :class:`des_crypt` hashes, + shoehorned into Django's hash format. + +These classes can be used directly as follows:: + + >>> from passlib.hash import django_salted_sha1 as handler + + >>> # encrypt password + >>> h = handler.encrypt("password") + >>> h + 'sha1$c6218$161d1ac8ab38979c5a31cbaba4a67378e7e60845' + + >>> # verify password + >>> handler.verify("password", h) True - >>> lms.verify("secret", h) #verify incorrect password + >>> handler.verify("eville", h) False +.. seealso:: the generic :ref:`PasswordHash usage examples ` + Interface --------- @@ -103,8 +174,8 @@ increasing the odds that a particular salt+password string will be present in a pre-computed tables of ascii digests. -Des Crypt -========= +Des Crypt Wrapper +================= .. autoclass:: django_des_crypt() @@ -132,10 +203,11 @@ Other Hashes ============ -.. autoclass:: django_disabled +.. autoclass:: django_disabled() .. note:: - Older versions of Django may also have - passwords encoded using :class:`~passlib.hash.hex_md5`, - though this has been deprecated by Django. + Some older (pre-1.0) versions of Django encoded + passwords using :class:`~passlib.hash.hex_md5`, + though this has been deprecated by Django, + and should become increasingly rare. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst passlib-1.6.1/docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst --- passlib-1.5.3/docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst 2012-05-03 16:36:58.000000000 +0000 @@ -13,24 +13,16 @@ .. seealso:: - * :doc:`passlib.hash.pbkdf2_digest ` - for some other PBKDF2-based hashes. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. - * :doc:`passlib.hash.cta_pbkdf2_sha1 ` + * :doc:`cta_pbkdf2_sha1 ` for another hash which looks almost exactly like this one. -Usage -===== -This class support both rounds and salts, -and can be used in the exact same manner -as :doc:`SHA-512 Crypt `. - Interface ========= .. autoclass:: dlitz_pbkdf2_sha1() -.. rst-class:: html-toggle - Format & Algorithm ================== @@ -47,11 +39,11 @@ stored as lowercase hexidecimal number with no zero-padding (in the example: ``2710`` or 10000 iterations). * :samp:`{salt}` is the salt string, which can be any number of characters, - drawn from the :ref:`hash64 charset ` + drawn from the :data:`hash64 charset ` (``.pPqsEwHD7MiECU0`` in the example). * :samp:`{checksum}` is 32 characters, which encode - the resulting 24-byte PBKDF2 derived key using :func:`~passlib.utils.adapted_b64_encode` + the resulting 24-byte PBKDF2 derived key using :func:`~passlib.utils.ab64_encode` (``b8TQ5AMQemtlaSgegw5Je.JBE3QQhLbO`` in the example). In order to generate the checksum, the password is first encoded into UTF-8 if it's unicode. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.fshp.rst passlib-1.6.1/docs/lib/passlib.hash.fshp.rst --- passlib-1.5.3/docs/lib/passlib.hash.fshp.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.fshp.rst 2012-08-02 18:50:00.000000000 +0000 @@ -4,50 +4,42 @@ .. index:: fshp -.. currentmodule:: passlib.hash - -The Fairly Secure Hashed Password (FSHP) scheme [#home]_ -is a cross-platform hash based on PBKDF1 [#pbk]_, and uses an LDAP-style hash format. -It features a variable length salt, variable rounds, and support for cryptographic -hashes from SHA-1 up to SHA-512. - -.. warning:: +.. note:: While the SHA-2 variants of PBKDF1 have no critical security vulnerabilities, PBKDF1 itself has been deprecated in favor of it's successor, PBKDF2. Furthermore, FSHP has been listed as insecure by it's author (for unspecified reasons); so this scheme should probably only be used to support existing hashes. -Usage -===== -This class supports the standard passlib options for rounds and salt, -as well as a special digest keyword for selecting the variant of FSHP to use. +.. currentmodule:: passlib.hash -This class can be used directly as follows:: +The Fairly Secure Hashed Password (FSHP) scheme [#home]_ +is a cross-platform hash based on PBKDF1 [#pbk]_, and uses an LDAP-style hash format. +It features a variable length salt, variable rounds, and support for cryptographic +hashes from SHA-1 up to SHA-512. +This class supports the standard Passlib options for rounds and salt, +as well as a special digest keyword for selecting the variant of FSHP to use. +It can be used directly as follows:: >>> from passlib.hash import fshp - >>> #generate new salt, encrypt password - >>> h = fshp.encrypt("password") - >>> h + >>> # generate new salt, encrypt password + >>> hash = fshp.encrypt("password") + >>> hash '{FSHP1|16|16384}PtoqcGUetmVEy/uR8715TNqKa8+teMF9qZO1lA9lJNUm1EQBLPZ+qPRLeEPHqy6C' - >>> #same, but with explict number of rounds, larger salt, and specific variant + >>> # the same, but with an explicit number of rounds, larger salt, and specific variant >>> fshp.encrypt("password", rounds=40000, salt_size=32, variant="sha512") - '{FSHP3|32|40000}cB8yE/CuADSgUTQZjWy+YTf/cvbU11D/rHNKiUiB6z4dIaO77U/rmNWpgZcZllZbCra5GJ8ZfFRNwCHirPqvYTAnbaQQeFQbWym/frRrRev3buoygFQRYexl4091Pc5m' + '{FSHP3|32|40000}cB8yE/CuADSgUTQZjWy+YTf/cvbU11D/rHNKiUiB6z4dIaO77U/rmNW + pgZcZllZbCra5GJ8ZfFRNwCHirPqvYTAnbaQQeFQbWym/frRrRev3buoygFQRYexl4091Pc5m' - >>> #check if hash is recognized - >>> fshp.identify(h) + >>> # verify password + >>> fshp.verify("password", hash) True - >>> #check if some other hash is recognized - >>> fshp.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') + >>> fshp.verify("secret", hash) False - >>> #verify correct password - >>> fshp.verify("password", h) - True - >>> fshp.verify("secret", h) #verify incorrect password - False +.. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= @@ -56,24 +48,24 @@ Format & Algorithm ================== -All of this scheme's hashes have the format: ``{FSHP||}``. +All of this scheme's hashes have the format: :samp:`\\{FSHP{variant}|{saltsize}|{rounds}\\}{data}`. A example hash (of ``password``) is: ``{FSHP1|16|16384}PtoqcGUetmVEy/uR8715TNqKa8+teMF9qZO1lA9lJNUm1EQBLPZ+qPRLeEPHqy6C`` -* :samp:`` is a decimal integer identifying the version of FSHP; +* :samp:`{variant}` is a decimal integer identifying the version of FSHP; in particular, which cryptographic hash function should be used to calculate the checksum. ``1`` in the example. (see the class description above for a list of possible values). -* :samp:`` is a decimal integer identifying the number of bytes +* :samp:`{saltsize}` is a decimal integer identifying the number of bytes in the salt. ``16`` in the example. -* :samp:`` is a decimal integer identifying the number +* :samp:`{rounds}` is a decimal integer identifying the number of rounds to apply when calculating the checksum (see below). ``16384`` in the example. -* :samp:`` is a base64-encoded string which, when decoded, +* :samp:`{data}` is a base64-encoded string which, when decoded, contains a salt string of the specified size, followed by the checksum. In the example, the data portion decodes to a salt value (in hexdecimal octets) of: @@ -117,10 +109,10 @@ as well as all known reference hashes. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through FSHP. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.grub_pbkdf2_sha512.rst passlib-1.6.1/docs/lib/passlib.hash.grub_pbkdf2_sha512.rst --- passlib-1.5.3/docs/lib/passlib.hash.grub_pbkdf2_sha512.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.grub_pbkdf2_sha512.rst 2012-08-01 17:05:50.000000000 +0000 @@ -15,14 +15,11 @@ .. seealso:: - * :doc:`passlib.hash.pbkdf2_digest ` - for some other PBKDF2-based hashes. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. -Usage -===== -These classes support both rounds and salts, -and can be used in the exact same manner -as :doc:`SHA-512 Crypt `. + * :doc:`passlib.hash.pbkdf2_{digest} ` -- + for some other PBKDF2-based hashes. Interface ========= @@ -59,16 +56,16 @@ >>> from passlib.hash import pbkdf2_sha512, grub_pbkdf2_sha512 - >>> #given a pbkdf2_sha512 hash... + >>> # given a pbkdf2_sha512 hash... >>> h = pbkdf2_sha512.encrypt("password") >>> h '$pbkdf2-sha512$6400$y6vYff3SihJiqumIrNXwGw$NobVwyUlVI52/Cvrguwli5fX6XgKHNUf7fWWS2VgoWEevaTCiZx4OCYhwGFwzUAuz/g1zQVSIf.9JEb0BEVEEA' - >>> #it can be parsed into options + >>> # it can be parsed into options >>> hobj = pbkdf2_sha512.from_string(h) >>> rounds, salt, chk = hobj.rounds, hobj.salt, hobj.checksum - >>> #and a new grub hash can be created + >>> # and a new grub hash can be created >>> gobj = grub_pbkdf2_sha512(rounds=rounds, salt=salt, checksum=chk) >>> g = gobj.to_string() >>> g diff -Nru passlib-1.5.3/docs/lib/passlib.hash.hex_digests.rst passlib-1.6.1/docs/lib/passlib.hash.hex_digests.rst --- passlib-1.5.3/docs/lib/passlib.hash.hex_digests.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.hex_digests.rst 2012-08-02 18:49:47.000000000 +0000 @@ -2,54 +2,61 @@ :samp:`passlib.hash.hex_{digest}` - Generic Hexdecimal Digests ============================================================== -.. currentmodule:: passlib.hash - -Some existing applications store passwords by storing them using -hexidecimal-encoded message digests, such as MD5 or SHA1. -Such schemes are *extremely* vulnerable to pre-computed brute-force attacks, -and should not be used in new applications. However, for the sake -of backwards compatibility when converting existing applications, -PassLib provides wrappers for few of the common hashes. - .. warning:: - To reiterate the above: Using a single round of any cryptographic hash (especially without a salt) is so insecure that it's barely better than plaintext. Do not use these schemes in new applications. +.. currentmodule:: passlib.hash -Usage -===== +Some existing applications store passwords by storing them using +hexidecimal-encoded message digests, such as MD5 or SHA1. +Such schemes are *extremely* vulnerable to pre-computed brute-force attacks, +and should not be used in new applications. However, for the sake +of backwards compatibility when converting existing applications, +Passlib provides wrappers for few of the common hashes. These classes all wrap the underlying hashlib implementations, -and are mainly useful only for plugging them into a :class:`~passlib.context.CryptContext`. -However, they can be used directly as follows:: +and can be used directly as follows:: - >>> from passlib.hash import hex_sha1 as hs + >>> from passlib.hash import hex_sha1 as hex_sha1 - >>> #encrypt password - >>> h = hs.encrypt("password") + >>> # encrypt password + >>> h = hex_sha1.encrypt("password") >>> h '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' - >>> hs.identify(h) #check if hash is recognized + >>> # verify correct password + >>> hex_sha1.verify("password", h) True - >>> hs.identify('JQMuyS6H.AGMo') #check if some other hash is recognized + + >>> # verify incorrect password + >>> hex_sha1.verify("secret", h) False - >>> hs.verify("password", h) #verify correct password - True - >>> hs.verify("secret", h) #verify incorrect password - False +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +.. index:: virtualbox; passwordhash Interface ========= -.. autoclass:: hex_md4() -.. autoclass:: hex_md5() -.. autoclass:: hex_sha1() -.. autoclass:: hex_sha256() -.. autoclass:: hex_sha512() +.. class:: hex_md4() +.. class:: hex_md5() +.. class:: hex_sha1() +.. class:: hex_sha256() +.. class:: hex_sha512() + + Each of these classes implements a plain hexidecimal encoded + message digest, using the relevant digest function from :mod:`!hashlib`, + and following the :ref:`password-hash-api`. + + They support no settings or other keywords. + +.. note:: + + Oracle VirtualBox's :command:`VBoxManager internalcommands passwordhash` command + uses :class:`hex_sha256`. Format & Algorithm ================== diff -Nru passlib-1.5.3/docs/lib/passlib.hash.ldap_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.ldap_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.ldap_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.ldap_crypt.rst 2012-08-01 17:05:50.000000000 +0000 @@ -1,6 +1,6 @@ -=========================================================== +================================================================ :samp:`passlib.hash.ldap_{crypt}` - LDAP crypt() Wrappers -=========================================================== +================================================================ .. currentmodule:: passlib.hash @@ -10,41 +10,40 @@ is somewhat different from the others. Instead of specifying a password hashing scheme, it's supposed to wrap the host OS's :func:`!crypt()`. - Being host-dependant, the actual hashes supported by this scheme may differ greatly between host systems. In order to provide uniform support across platforms, -Passlib defines a corresponding :samp:`ldap_{xxx}_crypt` scheme +Passlib defines a corresponding :samp:`ldap_{crypt-scheme}` class for each of the :ref:`standard unix hashes `. +These classes all wrap the underlying implementations documented +elsewhere in Passlib, and can be used directly as follows:: -.. seealso:: + >>> from passlib.hash import ldap_md5_crypt - * :doc:`passlib.hash.ldap_std` - the other standard LDAP hashes. + >>> # encrypt password + >>> hash = ldap_md5_crypt.encrypt("password") + >>> hash + '{CRYPT}$1$gwvn5BO0$3dyk8j.UTcsNUPrLMsU6/0' - * :mod:`!passlib.apps` for a :ref:`list of premade ldap contexts `. + >>> # verify password + >>> ldap_md5_crypt.verify("password", hash) + True + >>> ldap_md5_crypt.verify("secret", hash) + False -Usage -===== -These classes all wrap the underlying implementations, -and are mainly useful only for plugging them into a :class:`~passlib.context.CryptContext`. -However, they can be used directly as follows:: + >>> # determine if the underlying crypt() algorithm is supported + >>> # by your host OS, or if the builtin Passlib implementation is being used. + >>> # "os_crypt" - host supported; "builtin" - passlib version + >>> ldap_md5_crypt.get_backend() + "os_crypt" - >>> from passlib.hash import ldap_md5_crypt as lmc +.. seealso:: - >>> #encrypt password - >>> h = lmc.encrypt("password") - >>> h - '{CRYPT}$1$gwvn5BO0$3dyk8j.UTcsNUPrLMsU6/0' + * :ref:`password hash usage ` -- for more usage examples - >>> lmc.identify(h) #check if hash is recognized - True - >>> lmc.identify('JQMuyS6H.AGMo') #check if some other hash is recognized - False + * :doc:`ldap_{digest} ` -- for the other standard LDAP hashes. - >>> lmc.verify("password", h) #verify correct password - True - >>> lmc.verify("secret", h) #verify incorrect password - False + * :mod:`passlib.apps` -- for a list of :ref:`premade ldap contexts `. Interface ========= @@ -57,13 +56,7 @@ .. class:: ldap_sha512_crypt() All of these classes have the same interface as their corresponding - underlying hash (eg :class:`des_crypt`, :class:`md5_crypt`, etc). - -.. note:: - - In order to determine if a particular hash is actually supported - natively by your host OS, use an test such as - ``ldap_des_crypt.has_backend("os_crypt")`` or similar. + underlying hash (e.g. :class:`des_crypt`, :class:`md5_crypt`, etc). .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.ldap_other.rst passlib-1.6.1/docs/lib/passlib.hash.ldap_other.rst --- passlib-1.5.3/docs/lib/passlib.hash.ldap_other.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.ldap_other.rst 2012-05-03 16:36:58.000000000 +0000 @@ -10,7 +10,10 @@ .. seealso:: - :ref:`ldap-hashes` for a full list of RFC 2307 style hashes. + * :ref:`password hash usage ` -- + for examples of how to use these classes via the common hash interface. + + * :ref:`ldap-hashes` for a full list of RFC 2307 style hashes. Hexidecimal Digests =================== diff -Nru passlib-1.5.3/docs/lib/passlib.hash.ldap_pbkdf2_digest.rst passlib-1.6.1/docs/lib/passlib.hash.ldap_pbkdf2_digest.rst --- passlib-1.5.3/docs/lib/passlib.hash.ldap_pbkdf2_digest.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.ldap_pbkdf2_digest.rst 2012-05-03 16:36:58.000000000 +0000 @@ -6,7 +6,7 @@ .. currentmodule:: passlib.hash -PassLib provides three custom hash schemes based on the PBKDF2 [#pbkdf2]_ algorithm +Passlib provides three custom hash schemes based on the PBKDF2 [#pbkdf2]_ algorithm which are compatible with the :ref:`ldap hash format `: :class:`!ldap_pbkdf2_sha1`, :class:`!ldap_pbkdf2_sha256`, :class:`!ldap_pbkdf2_sha512`. They feature variable length salts, variable rounds. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.ldap_std.rst passlib-1.6.1/docs/lib/passlib.hash.ldap_std.rst --- passlib-1.5.3/docs/lib/passlib.hash.ldap_std.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.ldap_std.rst 2012-05-03 16:36:58.000000000 +0000 @@ -9,42 +9,30 @@ This includes ``{MD5}``, ``{SMD5}``, ``{SHA}``, ``{SSHA}``. These schemes range from somewhat to very insecure, and should not be used except when required. - -.. note:: - - RFC 2307 also specifies a ``{CRYPT}`` scheme, - which is supposed to wrap the host OS's :func:`!crypt()`. - Being host-dependant, this scheme is somewhat different, - and is details in :doc:`passlib.hash.ldap_crypt`. - -.. seealso:: - - * :doc:`passlib.hash.ldap_crypt` - - * :mod:`!passlib.apps` for a :ref:`list of premade ldap contexts `. - -Usage -===== These classes all wrap the underlying hashlib implementations, -and are mainly useful only for plugging them into a :class:`~passlib.context.CryptContext`. -However, they can be used directly as follows:: +and are can be used directly as follows:: >>> from passlib.hash import ldap_salted_md5 as lsm - >>> #encrypt password - >>> h = lsm.encrypt("password") - >>> h + >>> # encrypt password + >>> hash = lsm.encrypt("password") + >>> hash '{SMD5}OqsUXNHIhHbznxrqHoIM+ZT8DmE=' - >>> lms.identify(h) #check if hash is recognized + >>> # verify password + >>> lms.verify("password", hash) True - >>> lms.identify('JQMuyS6H.AGMo') #check if some other hash is recognized + >>> lms.verify("secret", hash) False - >>> lms.verify("password", h) #verify correct password - True - >>> lms.verify("secret", h) #verify incorrect password - False +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`ldap_{crypt} ` -- + LDAP ``{CRYPT}`` wrappers for common Unix hash algorithms. + + * :mod:`passlib.apps` -- for a list of :ref:`premade ldap contexts `. Plain Hashes ============ @@ -62,7 +50,7 @@ These hashes have the format :samp:`{prefix}{checksum}`. -* :samp:`{prefix}` is `{MD5}` for ldap_md5, and `{SHA}` for ldap_sha1. +* :samp:`{prefix}` is ``{MD5}`` for ldap_md5, and ``{SHA}`` for ldap_sha1. * :samp:`{checksum}` is the base64 encoding of the raw message digest of the password, using the appropriate digest algorithm. @@ -77,10 +65,10 @@ These hashes have the format :samp:`{prefix}{data}`. -* :samp:`{prefix}` is `{SMD5}` for ldap_salted_md5, - and `{SSHA}` for ldap_salted_sha1. +* :samp:`{prefix}` is ``{SMD5}`` for ldap_salted_md5, + and ``{SSHA}`` for ldap_salted_sha1. * :samp:`{data}` is the base64 encoding of :samp:`{checksum}{salt}`; - and in turn :samp:`{salt}` is a 4 byte binary salt, + and in turn :samp:`{salt}` is a multi-byte binary salt, and :samp:`{checksum}` is the raw digest of the the string :samp:`{password}{salt}`, using the appropriate digest algorithm. @@ -113,13 +101,22 @@ This handler does not hash passwords at all, rather it encoded them into UTF-8. -The only difference between this class and :class:`passlib.hash.plaintext` -is that this class will NOT recognize any strings using +The only difference between this class and :class:`~passlib.hash.plaintext` +is that this class will NOT recognize any strings that use the ``{SCHEME}HASH`` format. +Deviations +========== + +* The salt size for the salted digests appears to vary between applications. + While OpenLDAP is fixed at 4 bytes, some systems appear to use 8 or more. + As of 1.6, Passlib can accept and generate strings with salts between 4-16 bytes, + though various servers may differ in what they can handle. .. rubric:: Footnotes .. [#pwd] The manpage for :command:`slappasswd` - ``_. .. [#rfc] The basic format for these hashes is laid out in RFC 2307 - ``_ + +.. [#] OpenLDAP hash documentation - ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.lmhash.rst passlib-1.6.1/docs/lib/passlib.hash.lmhash.rst --- passlib-1.5.3/docs/lib/passlib.hash.lmhash.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.lmhash.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,157 @@ +.. index:: LAN Manager hash, Windows; LAN Manager hash + +================================================================== +:class:`passlib.hash.lmhash` - LanManager Hash +================================================================== + +.. versionadded:: 1.6 + +.. warning:: + + This scheme has been deprecated since Windows NT, and is notoriously weak. + It should be used for compatibility with existing systems; + **do not use** in new code. + +.. currentmodule:: passlib.hash + +This class implements the LanManager Hash (aka *LanMan* or *LM* hash). +It was used by early versions of Microsoft Windows to store user passwords, +until it was supplanted (though not entirely replaced) by +the :doc:`nthash ` algorithm in Windows NT. +It continues to crop up in production due to it's integral role +in the legacy NTLM authentication protocol. +This class can be used directly as follows:: + + >>> from passlib.hash import lmhash + + >>> # encrypt password + >>> h = lmhash.encrypt("password") + >>> h + 'e52cac67419a9a224a3b108f3fa6cb6d' + + >>> # verify correct password + >>> lmhash.verify("password", h) + True + >>> # verify incorrect password + >>> lmhash.verify("secret", h) + False + +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +Interface +========= +.. autoclass:: lmhash() + +Issues with Non-ASCII Characters +-------------------------------- +Passwords containing only ``ascii`` characters should hash and compare +correctly across all LMhash implementations. However, due to historical +issues, no two LMhash implementations handle non-``ascii`` characters in quite +the same way. While Passlib makes every attempt to behave as close to correct +as possible, the meaning of "correct" is dependant on the software you are +interoperating with. If you think you will have passwords containing +non-``ascii`` characters, please read the `Deviations`_ section (below) for +details about the known interoperability issues. It's a mess of codepages. + +.. rst-class:: html-toggle + +Format & Algorithm +================== +A LM hash consists of 32 hexidecimal digits, +which encode the 16 byte digest. An example hash (of ``password``) is +``e52cac67419a9a224a3b108f3fa6cb6d``. + +The digest is calculated as follows: + +1. First the password should be converted to uppercase, and encoded + to bytes using the "OEM Codepage" used [#cp]_ by the specific release of + Windows that the host or target server is running. + + For pure-ASCII passwords, this step can be performed as normal + using the ``us-ascii`` encoding. For passwords with non-ASCII + characters, this step is fraught with compatibility issues + and border cases (see `Deviations`_ for details). + +2. The password is then truncated or NULL padded to 14 bytes, as appropriate. + +3. The first 7 bytes of the password in step 2 are used as a key, + to DES encrypt the constant ``KGS!@#$%``, resulting + in the first 8 bytes of the final digest. + +4. Step 4 is repeated using the second 7 bytes of the password from step 2, + resulting in the second 8 bytes of the final digest. + +5. The combined digests from 3 and 4 are then encoded to hexidecimal. + +Security Issues +=============== +Due to this myriad of flaws, high-speed password cracking software +dedicated to LMHASH exists, and the algorithm should be considered broken: + +* It has no salt, making hashes easily pre-computable. + +* It limits the password to 14 characters, and converts the password to + uppercase before hashing, greatly reducing the keyspace. + +* By breaking the password into two independant chunks, + they can be attacked independantly and simultaneously. + +* The independance of the chunks reveals significant information + about the original password: The second 8 bytes of the digest + are the same for all passwords < 8 bytes; and for passwords + of 8-9 characters, the second chunk can be broken *much* faster, + revealing part of the password, and reducing the likely + keyspace for the first chunk. + +Deviations +========== +Passlib's implementation differs from others in a few ways, all related to +the handling of non-ASCII characters. Future releases of Passlib may update +the implementation as new information comes up. + +* Unicode Policy: + + Officially, unicode passwords should be encoded using the "OEM Codepage" + used [#cp]_ by the specific release of Windows that the host or target server + is running. Common encodings include ``cp437`` (used by the English + edition of Windows XP), ``cp580`` (used by many Western European editions + of XP), and ``cp866`` (used by many Eastern European editions of XP). + Complicating matters further, some third-party implementations are known + to use encodings such as ``latin-1`` and ``utf-8``, which cause + the non-ASCII characters to have different hashes entirely. + + Thus the application must decide which encoding to use, if it wants + to provide support for non-ASCII passwords. Passlib uses ``cp437`` as a + default, but this may need to be overridden via + ``lmhash.encrypt(secret, encoding="some-other-codec")``. + All known encodings are ``us-ascii``-compatible, so for ASCII passwords, + the default should be sufficient. + +* Upper Case Conversion: + + Once critical step in the LMHASH algorithm is converting the password + to upper case. While ASCII characters are converted to uppercase as normal, + non-ASCII characters are converted in implementation dependant ways: + + Windows systems encode the password first, and then + convert it to uppercase using a codepage-dependant table. + For the most part these tables appear to agree with the Unicode specification, + but there are some codepoints where they deviate (for example, + Unicode uppercases U+00B5 -> U+039C, but ``cp437`` leaves it unchanged + [#uc]_). + + Most third-party implementations (Passlib included) choose to uppercase + non-ASCII characters according to the Unicode specification, and then + encode the password; despite the border cases where the hash would not match + the official windows hash. + +.. rubric:: Footnotes + +.. [#] Article used as reference for algorithm - + ``_. + +.. [#cp] The OEM codepage used by specific Window XP (and earlier releases) + can be found at ``_. + +.. [#uc] Online discussion dealing with upper-case encoding issues - + ``_. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.md5_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.md5_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.md5_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.md5_crypt.rst 2012-06-27 19:56:44.000000000 +0000 @@ -1,39 +1,69 @@ +.. index:: Cisco; Type 5 hash + ================================================================== :class:`passlib.hash.md5_crypt` - MD5 Crypt ================================================================== +.. warning:: + + As of 2012-6-7, this algorithm is "no longer considered safe" + by it's author [#phk]_, citing the increased + speed of the MD5 hash on modern hardware, and MD5-Crypt's + lack of a variable time-cost parameter. See Passlib's + :ref:`recommended hashes ` for a replacement. + .. currentmodule:: passlib.hash This algorithm was developed for FreeBSD in 1994 by Poul-Henning Kamp, to replace the aging :class:`passlib.hash.des_crypt`. It has since been adopted by a wide variety of other Unix flavors, and is found in many other contexts as well. Due to it's origins, it's sometimes referred to as "FreeBSD MD5 Crypt". -Security-wise it is considered to be steadily weakening (due to fixed cost), -and most unix flavors have since replaced with with stronger schemes, -such as :class:`~passlib.hash.sha512_crypt` and :class:`~passlib.hash.bcrypt`. - -Usage -===== -PassLib provides an md5_crypt class, which can be can be used directly as follows:: - - >>> from passlib.hash import md5_crypt as mc - - >>> mc.encrypt("password") #generate new salt, encrypt password +Security-wise it should now be considered weak, +and most Unix flavors have since replaced it with stronger schemes +(such as :class:`~passlib.hash.sha512_crypt` and :class:`~passlib.hash.bcrypt`). + +This is also referred to on Cisco IOS systems as a "type 5" hash. +The format and algorithm are identical, though Cisco seems to require +4 salt characters instead of the full 8 characters +used by most systems [#cisco]_. + +The :class:`!md5_crypt` class can be can be used directly as follows:: + + >>> from passlib.hash import md5_crypt + + >>> # generate new salt, encrypt password + >>> h = md5_crypt.encrypt("password") + >>> h '$1$3azHgidD$SrJPt7B.9rekpmwJwtON31' - >>> mc.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if hash is recognized + >>> # verify the password + >>> md5_crypt.verify("password", h) True - >>> mc.identify('JQMuyS6H.AGMo') #check if some other hash is recognized + >>> md5_crypt.verify("secret", h) False - >>> mc.verify("password", '$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #verify correct password - True - >>> mc.verify("secret", '$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #verify incorrect password - False + >>> # encrypt password using cisco-compatible 4-char salt + >>> md5_crypt.encrypt("password", salt_size=4) + '$1$wu98$9UuD3hvrwehnqyF1D548N0' + +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`apr_md5_crypt ` -- Apache's variant of this algorithm. Interface ========= -.. autoclass:: md5_crypt(checksum=None, salt=None, strict=False) +.. autoclass:: md5_crypt() + +.. note:: + + This class will use the first available of two possible backends: + + * stdlib :func:`crypt()`, if the host OS supports MD5-Crypt (most Unix systems). + * a pure python implementation of MD5-Crypt built into Passlib. + + You can see which backend is in use by calling the :meth:`get_backend()` method. Format ====== @@ -81,7 +111,7 @@ 9. Add the password to digest A. -10. Add the constant string ``$1$`` to digest A. +10. Add the constant string ``$1$`` to digest A. (The Apache variant of MD5-Crypt uses ``$apr1$`` instead, this is the only change made by this variant). @@ -122,21 +152,20 @@ following order: ``12,6,0,13,7,1,14,8,2,15,9,3,5,10,4,11``. 18. Encode the resulting 16 byte string into a 22 character - :mod:`hash 64 `-encoded string + :data:`hash64 `-encoded string (the 2 msb bits encoded by the last hash64 character are used as 0 padding). This results in the portion of the md5 crypt hash string referred to as :samp:`{checksum}` in the format section. Security Issues =============== -MD5-Crypt has a couple of issues which have weakened it, -though it is not yet considered broken: +MD5-Crypt has a couple of issues which have weakened severely: * It relies on the MD5 message digest, for which theoretical pre-image attacks exist [#f2]_. However, not only is this attack still only theoretical, but none of MD5's weaknesses have been show to affect MD5-Crypt's security. -* The fixed number of rounds, combined with the availability - of high-throughput MD5 implementations, means this algorithm +* More seriously, it's fixed number of rounds (combined with the availability + of high-throughput MD5 implementations) means this algorithm is increasingly vulnerable to brute force attacks. It is this issue which has motivated it's replacement by new algorithms such as :class:`~passlib.hash.bcrypt` @@ -144,14 +173,14 @@ Deviations ========== -PassLib's implementation of md5-crypt differs from the reference implementation (and others) in two ways: +Passlib's implementation of md5-crypt differs from the reference implementation (and others) in two ways: * Restricted salt string character set: The underlying algorithm can unambigously handle salt strings which contain any possible byte value besides ``\x00`` and ``$``. - However, PassLib strictly limits salts to the - :mod:`hash 64 ` character set, + However, Passlib strictly limits salts to the + :data:`hash64 ` character set, as nearly all implementations of md5-crypt generate and expect salts containing those characters, but may have unexpected behaviors for other character values. @@ -165,10 +194,10 @@ as well as all known reference hashes. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through md5-crypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes @@ -178,3 +207,8 @@ .. [#f2] Security issues with MD5 - ``_. + +.. [#cisco] Note about Cisco Type 5 salt size - + ``_. + +.. [#phk] Deprecation Announcement from Poul-Henning Kamp - ``_. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.msdcc.rst passlib-1.6.1/docs/lib/passlib.hash.msdcc.rst --- passlib-1.5.3/docs/lib/passlib.hash.msdcc.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.msdcc.rst 2012-08-01 17:05:50.000000000 +0000 @@ -0,0 +1,98 @@ +.. index:: + single: Windows; Domain Cached Credentials + see: mscash; msdcc + see: mscache; msdcc + +====================================================================== +:class:`passlib.hash.msdcc` - Windows' Domain Cached Credentials +====================================================================== + +.. versionadded:: 1.6 + +.. warning:: + + This hash is not very secure, and should mainly be used to verify + existing cached credentials. + +.. currentmodule:: passlib.hash + +This class implements the DCC (Domain Cached Credentials) hash, used +by Windows to cache and verify remote credentials when the relevant +server is unavailable. It is known by a number of other names, +including "mscache" and "mscash" (Microsoft CAched haSH). Security wise +it is not particularly strong, as it's little more than :doc:`nthash ` +salted with a username. It was replaced by :doc:`msdcc2 ` +in Windows Vista. +This class can be used directly as follows:: + + >>> from passlib.hash import msdcc + + >>> # encrypt password using specified username + >>> hash = msdcc.encrypt("password", user="Administrator") + >>> hash + '25fd08fa89795ed54207e6e8442a6ca0' + + >>> # verify correct password + >>> msdcc.verify("password", hash, user="Administrator") + True + >>> # verify correct password w/ wrong username + >>> msdcc.verify("password", hash, user="User") + False + >>> # verify incorrect password + >>> msdcc.verify("letmein", hash, user="Administrator") + False + +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`msdcc2 ` -- the successor to this hash + +Interface +========= +.. autoclass:: msdcc() + +.. rst-class:: html-toggle + +Format & Algorithm +================== +Much like :class:`!lmhash` and :class:`!nthash`, MS DCC hashes +consists of a 16 byte digest, usually encoded as 32 hexidecimal characters. +An example hash (of ``"password"`` with the account ``"Administrator"``) is +``25fd08fa89795ed54207e6e8442a6ca0``. + +The digest is calculated as follows: + +1. The password is encoded using ``UTF-16-LE``. +2. The MD4 digest of step 1 is calculated. + (The result of this step is identical to the :class:`~passlib.hash.nthash` + of the password). +3. The unicode username is converted to lowercase, + and encoded using ``UTF-16-LE``. + This should be just the plain username (e.g. ``User`` + not ``SOMEDOMAIN\\User``) +4. The username from step 3 is appended to the + digest from step 2; and the MD4 digest of the result + is calculated. +5. The result of step 4 is encoded into hexidecimal, + this is the DCC hash. + +Security Issues +=============== +This algorithm is should not be used for any purpose besides +manipulating existing DCC v1 hashes, due to the following flaws: + +* It's use of the username as a salt value (and lower-case at that), + means that common usernames (e.g. ``Administrator``) will occur + more frequently as salts, weakening the effectiveness of the salt in + foiling pre-computed tables. + +* The MD4 message digest has been severely compromised by collision and + preimage attacks. + +* Efficient brute-force attacks on MD4 exist. + +.. rubric:: Footnotes + +.. [#] Description of DCC v1 algorithm - + ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.msdcc2.rst passlib-1.6.1/docs/lib/passlib.hash.msdcc2.rst --- passlib-1.5.3/docs/lib/passlib.hash.msdcc2.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.msdcc2.rst 2012-07-11 17:32:00.000000000 +0000 @@ -0,0 +1,106 @@ +.. index:: + single: Windows; Domain Cached Credentials v2 + +====================================================================== +:class:`passlib.hash.msdcc2` - Windows' Domain Cached Credentials v2 +====================================================================== + +.. versionadded:: 1.6 + +.. currentmodule:: passlib.hash + +This class implements the DCC2 (Domain Cached Credentials version 2) hash, used +by Windows Vista and newer to cache and verify remote credentials when the relevant +server is unavailable. It is known by a number of other names, +including "mscache2" and "mscash2" (Microsoft CAched haSH). It replaces +the weaker :doc:`msdcc v1` hash used by previous releases +of Windows. Security wise it is not particularly weak, but due to it's +use of the username as a salt, it should probably not be used for anything +but verifying existing cached credentials. +This class can be used directly as follows:: + + >>> from passlib.hash import msdcc2 + + >>> # encrypt password using specified username + >>> hash = msdcc2.encrypt("password", user="Administrator") + >>> hash + '4c253e4b65c007a8cd683ea57bc43c76' + + >>> # verify correct password + >>> msdcc2.verify("password", hash, user="Administrator") + True + >>> # verify correct password w/ wrong username + >>> msdcc2.verify("password", hash, user="User") + False + >>> # verify incorrect password + >>> msdcc2.verify("letmein", hash, user="Administrator") + False + +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`msdcc ` -- the predecessor to this hash + +Interface +========= +.. autoclass:: msdcc2() + +.. rst-class:: html-toggle + +Format & Algorithm +================== +Much like :class:`!lmhash`, :class:`!nthash`, and :class:`!msdcc`, +MS DCC v2 hashes consists of a 16 byte digest, usually encoded as 32 +hexidecimal characters. An example hash (of ``"password"`` with the +account ``"Administrator"``) is ``4c253e4b65c007a8cd683ea57bc43c76``. + +The digest is calculated as follows: + +1. The password is encoded using ``UTF-16-LE``. +2. The MD4 digest of step 1 is calculated. + (The result of this is identical to the :class:`~passlib.hash.nthash` + digest of the password). +3. The unicode username is converted to lowercase, + and encoded using ``UTF-16-LE``. + This should be just the plain username (e.g. ``User`` + not ``SOMEDOMAIN\\User``) +4. The username from step 3 is appended to the + digest from step 2; and the MD4 digest of the result + is calculated (The result of this is identicial to the + :class:`~passlib.hash.msdcc` digest). +5. :func:`PBKDF2-HMAC-SHA1 ` is then invoked, + using the result of step 4 as the secret, the username from step 3 as + the salt, 10240 rounds, and resulting in a 16 byte digest. +6. The result of step 5 is encoded into hexidecimal; + this is the DCC2 hash. + +Security Issues +=============== +This hash is essentially :doc:`msdcc v1 ` with a fixed-round PBKDF2 function +wrapped around it. The number of rounds of PBKDF2 is currently +sufficient to make this a semi-reasonable way to store passwords, +but the use of the lowercase username as a salt, and the fact +that the rounds can't be increased, means this hash is not particularly +future-proof, and should not be used for new applications. + +Deviations +========== + +* Max Password Size + + Windows appears to enforce a maximum password size, + but the actual value of this limit is unclear; sources + report it to be set at assorted values from 26 to 128 characters, + and it may in fact vary between Windows releases. + The one consistent peice of information is that + passwords above the limit are simply not allowed (rather + than truncated ala :class:`~passlib.hash.des_crypt`). + Because of this, Passlib does not currently enforce a size limit: + any hashes this class generates should be correct, provided Windows + is willing to accept a password of that size. + +.. rubric:: Footnotes + +.. [#] Description of DCC v2 algorithm - + ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.mssql2000.rst passlib-1.6.1/docs/lib/passlib.hash.mssql2000.rst --- passlib-1.5.3/docs/lib/passlib.hash.mssql2000.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.mssql2000.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,97 @@ +================================================================== +:class:`passlib.hash.mssql2000` - MS SQL 2000 password hash +================================================================== + +.. versionadded:: 1.6 + +.. warning:: + + This hash is not very secure, and should not be used for any purposes + besides manipulating existing MSSQL 2000 password hashes. + +.. currentmodule:: passlib.hash + +This class implements the hash algorithm used by Microsoft SQL Server 2000 +to store it's user account passwords, until it was replaced +by a slightly more secure variant (:class:`~passlib.hash.mssql2005`) +in MSSQL 2005. +This class can be used directly as follows:: + + >>> from passlib.hash import mssql2000 as m20 + + >>> # encrypt password + >>> h = m20.encrypt("password") + >>> h + '0x0100200420C4988140FD3920894C3EDC188E94F428D57DAD5905F6CC1CBAF950CAD4C63F272B2C91E4DEEB5E6444' + + >>> # verify correct password + >>> m20.verify("password", h) + True + >>> m20.verify("letmein", h) + False + +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`mssql2005 ` -- the successor to this hash. + +Interface +========= +.. autoclass:: mssql2000() + +.. rst-class:: html-toggle + +Format & Algorithm +================== +MSSQL 2000 hashes are usually presented as a series of 92 upper-case +hexidecimal characters, prefixed by ``0x``. An example MSSQL 2000 hash +(of ``"password"``):: + + 0x0100200420C4988140FD3920894C3EDC188E94F428D57DAD5905F6CC1CBAF950CAD4C63F272B2C91E4DEEB5E6444 + +This encodes 46 bytes of raw data, consisting of: + +* a 2-byte constant ``0100`` +* 4 byte of salt (``200420C4`` in the example) +* the first 20 byte digest (``988140FD3920894C3EDC188E94F428D57DAD5905`` + in the example). +* a second 20 byte digest (``F6CC1CBAF950CAD4C63F272B2C91E4DEEB5E6444`` + in the example). + +The first digest is generated by encoding the unicode password using +``UTF-16-LE``, and calculating ``SHA1(encoded_secret + salt)``. + +The second digest is generated the same as the first, +except that the password is converted to upper-case first. + +Only the second digest is used when verifying passwords (and hence the hash +is case-insensitive). The first digest is presumably for forward-compatibility: +MSSQL 2005 removed the second digest, and thus became case sensitive. + +.. note:: + + MSSQL 2000 hashes do not actually have a native textual format, as they + are stored as raw bytes in an SQL table. However, when external programs + deal with them, MSSQL generally encodes raw bytes as upper-case hexidecimal, + prefixed with ``0x``. This is the representation Passlib uses. + +Security Issues +=============== +This algorithm is reasonably weak, and shouldn't be used for any +purpose besides manipulating existing MSSQL 2000 hashes, due to the +following flaws: + +* The fact that it is case insensitive greatly reduces the keyspace that + must be searched by brute-force or pre-computed attacks. + +* It's simplicity, and years of research on high-speed SHA1 + implementations, makes efficient brute force attacks much more feasible. + +.. rubric:: Footnotes + +.. [#] Overview hash algorithms used by MSSQL - + ``_. + +.. [#] Description of MSSQL 2000 algorithm - + ``_. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.mssql2005.rst passlib-1.6.1/docs/lib/passlib.hash.mssql2005.rst --- passlib-1.5.3/docs/lib/passlib.hash.mssql2005.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.mssql2005.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,86 @@ +================================================================== +:class:`passlib.hash.mssql2005` - MS SQL 2005 password hash +================================================================== + +.. versionadded:: 1.6 + +.. warning:: + + This hash is not very secure, and should not be used for any purposes + besides manipulating existing MSSQL 2005 password hashes. + +.. currentmodule:: passlib.hash + +This class implements the hash algorithm used by Microsoft SQL Server 2005 +to store it's user account passwords, replacing the slightly less secure +:class:`~passlib.hash.mssql2000` variant. +This class can be used directly as follows:: + + >>> from passlib.hash import mssql2005 as m25 + + >>> # encrypt password + >>> h = m25.encrypt("password") + >>> h + '0x01006ACDF9FF5D2E211B392EEF1175EFFE13B3A368CE2F94038B' + + >>> # verify password + >>> m25.verify("password", h) + True + >>> m25.verify("letmein", h) + False + +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`mssql2000 ` -- the predecessor to this hash. + +Interface +========= +.. autoclass:: mssql2005() + +.. rst-class:: html-toggle + +Format & Algorithm +================== +MSSQL 2005 hashes are usually presented as a series of 52 upper-case +hexidecimal characters, prefixed by ``0x``. An example MSSQL 2005 hash +(of ``"password"``):: + + 0x01006ACDF9FF5D2E211B392EEF1175EFFE13B3A368CE2F94038B + +This encodes 26 bytes of raw data, consisting of: + +* a 2-byte constant ``0100`` +* 4 byte of salt (``6ACDF9FF`` in the example) +* 20 byte digest (``5D2E211B392EEF1175EFFE13B3A368CE2F94038B`` + in the example). + +The digest is generated by encoding the unicode password using +``UTF-16-LE``, and calculating ``SHA1(encoded_secret + salt)``. + +This format and algorithm is identical to :doc:`mssql2000 `, +except that this hash omits the 2nd case-insensitive +digest used by MSSQL 2000. + +.. note:: + + MSSQL 2005 hashes do not actually have a native textual format, as they + are stored as raw bytes in an SQL table. However, when external programs + deal with them, MSSQL generally encodes raw bytes as upper-case hexidecimal, + prefixed with ``0x``. This is the representation Passlib uses. + +Security Issues +=============== +This algorithm is reasonably weak, and shouldn't be used for any +purpose besides manipulating existing MSSQL 2005 hashes. This mainly due to +it's simplicity, and years of research on high-speed SHA1 +implementations, which makes efficient brute force attacks feasible. + +.. rubric:: Footnotes + +.. [#] Overview hash algorithms used by MSSQL - + ``_. + +.. [#] Description of MSSQL 2000/2005 algorithm - + ``_. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.mysql323.rst passlib-1.6.1/docs/lib/passlib.hash.mysql323.rst --- passlib-1.5.3/docs/lib/passlib.hash.mysql323.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.mysql323.rst 2012-08-01 17:25:05.000000000 +0000 @@ -1,4 +1,4 @@ -.. index:: mysql; OLD_PASSWORD() +.. index:: MySQL; OLD_PASSWORD() ======================================================================== :class:`passlib.hash.mysql323` - MySQL 3.2.3 password hash @@ -6,43 +6,38 @@ .. currentmodule:: passlib.hash - -This class implements the first of MySQL's password hash functions, -used to store it's user account passwords. Introduced in MySQL 3.2.3 -under the function ``PASSWORD()``, this function was renamed -to ``OLD_PASSWORD()`` under MySQL 4.1, when a newer password -hash algorithm was introduced (see :class:`~passlib.hash.mysql41`). - .. warning:: This algorithm is extremely weak, and should not be used for any purposes besides manipulating existing Mysql 3.2.3-4.0 password hashes. -.. seealso:: - - :mod:`!passlib.apps` for a list of predefined :ref:`mysql contexts `. - -Usage -===== +This class implements the first of MySQL's password hash functions, +used to store it's user account passwords. Introduced in MySQL 3.2.3 +under the function ``PASSWORD()``, this function was renamed +to ``OLD_PASSWORD()`` under MySQL 4.1, when a newer password +hash algorithm was introduced (see :class:`~passlib.hash.mysql41`). Users will most likely find the frontends provided by :mod:`passlib.apps` to be more useful than accessing this class directly. That aside, this class can be used as follows:: - >>> from passlib.hash import mysql323 as mold + >>> from passlib.hash import mysql323 - >>> mold.encrypt("password") #encrypt password + >>> # encrypt password + >>> mysql323.encrypt("password") '5d2e19393cc5ef67' - >>> mold.identify('5d2e19393cc5ef67') #check if hash is recognized + >>> # verify correct password + >>> mysql323.verify("password", '5d2e19393cc5ef67') True - >>> mold.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if another type of hash is recognized + >>> mysql323.verify("secret", '5d2e19393cc5ef67') False - >>> mold.verify("password", '5d2e19393cc5ef67') #verify correct password - True - >>> mold.verify("secret", '5d2e19393cc5ef67') #verify incorrect password - False +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :mod:`passlib.apps` -- for a list of predefined :ref:`mysql contexts `. Interface ========= @@ -52,8 +47,8 @@ ================== A mysql-323 password hash consists of 16 hexidecimal digits, directly encoding the 64 bit checksum. MySQL always uses -lower-case letters, and so does PassLib -(though PassLib will recognize upper case letters as well). +lower-case letters, and so does Passlib +(though Passlib will recognize upper case letters as well). The algorithm used is extremely simplistic, for details, see the source implementation in the footnotes [#f1]_. @@ -67,7 +62,7 @@ .. rubric:: Footnotes -.. [#f1] Source of implementation used by passlib - +.. [#f1] Source of implementation used by Passlib - ``_ .. [#f2] Mysql document describing transition - diff -Nru passlib-1.5.3/docs/lib/passlib.hash.mysql41.rst passlib-1.6.1/docs/lib/passlib.hash.mysql41.rst --- passlib-1.5.3/docs/lib/passlib.hash.mysql41.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.mysql41.rst 2012-05-03 16:36:58.000000000 +0000 @@ -1,4 +1,4 @@ -.. index:: mysql; PASSWORD() +.. index:: MySQL; PASSWORD() ===================================================================== :class:`passlib.hash.mysql41` - MySQL 4.1 password hash @@ -6,28 +6,26 @@ .. currentmodule:: passlib.hash -This class implements the second of MySQL's password hash functions, -used to store it's user account passwords. Introduced in MySQL 4.1.1 -under the function ``PASSWORD()``, it replaced the previous -algorithm (:class:`~passlib.hash.mysql323`) as the default -used by MySQL, and is still in active use under MySQL 5. - .. warning:: This algorithm is extremely weak, and should not be used for any purposes besides manipulating existing Mysql 4.1+ password hashes. +This class implements the second of MySQL's password hash functions, +used to store it's user account passwords. Introduced in MySQL 4.1.1 +under the function ``PASSWORD()``, it replaced the previous +algorithm (:class:`~passlib.hash.mysql323`) as the default +used by MySQL, and is still in active use under MySQL 5. +Users will most likely find the frontends provided by :mod:`passlib.apps` +to be more useful than accessing this class directly. + .. seealso:: - :mod:`!passlib.apps` for a :ref:`list of premade mysql contexts `. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. -Usage -===== -Users will most likely find the frontends provided by :mod:`passlib.apps` -to be more useful than accessing this class directly. -That aside, this class can be used in the same manner -as :class:`~passlib.hash.mysql323`. + * :mod:`passlib.apps` for a list of :ref:`premade mysql contexts `. Interface ========= @@ -39,7 +37,7 @@ by 40 hexidecimal digits, directly encoding the 160 bit checksum. An example hash (of ``password``) is ``*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19``. MySQL always uses upper-case letters, -and so does PassLib (though PassLib will recognize lower-case letters as well). +and so does Passlib (though Passlib will recognize lower-case letters as well). The checksum is calculated simply, as the SHA1 hash of the SHA1 hash of the password, which is then encoded into hexidecimal. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.nthash.rst passlib-1.6.1/docs/lib/passlib.hash.nthash.rst --- passlib-1.5.3/docs/lib/passlib.hash.nthash.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.nthash.rst 2012-05-03 16:36:58.000000000 +0000 @@ -1,63 +1,82 @@ +.. index:: Windows; NT hash + ================================================================== -:class:`passlib.hash.nthash` - Windows NT-HASH for Unix +:class:`passlib.hash.nthash` - Windows' NT-HASH ================================================================== -.. currentmodule:: passlib.hash - -This class implements the Windows NT-HASH algorithm, -encoded in a manner compatible with the :ref:`modular-crypt-format`. -It is found on some unix systems where the administrator has decided -to store user passwords in a manner compatible with the SMB/CIFS protocol. +.. versionadded:: 1.6 .. warning:: - This scheme is notoriously weak (since it's based on :mod:`~passlib.utils.md4`). - Online tables exist for quickly performing pre-image attacks on this scheme. - **Do not use** in new code. Stop using in old code if possible. + This scheme is very weak, the :mod:`~passlib.utils.md4` digest + it is based on has been severely compromised for many years. + It should be used for compatibility with existing systems; + **do not use** in new code. -Usage -===== +.. currentmodule:: passlib.hash +This class implements the NT-HASH algorithm, used by Microsoft Windows NT +and successors to store user account passwords, supplanting +the much weaker :doc:`lmhash ` algorithm. This class can be used directly as follows:: - >>> from passlib.hash import nthash as nt + >>> from passlib.hash import nthash - >>> #encrypt password - >>> h = nt.encrypt("password") + >>> # encrypt password + >>> h = nthash.encrypt("password") >>> h - '$3$$8846f7eaee8fb117ad06bdd830b7586c' + '8846f7eaee8fb117ad06bdd830b7586c' - >>> nt.identify(h) #check if hash is recognized + >>> # verify password + >>> nthash.verify("password", h) True - >>> nt.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if some other hash is recognized + >>> nthash.verify("secret", h) False - >>> nt.verify("password", h) #verify correct password - True - >>> nt.verify("secret", h) #verify incorrect password - False +.. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= -.. autoclass:: nthash +.. autoclass:: nthash() -In addition to the standard methods, this class exposes the following: +Format & Algorithm +================== +A nthash consists of 32 hexidecimal digits, which encode the digest. +An example hash (of ``password``) is ``8846f7eaee8fb117ad06bdd830b7586c``. -.. staticmethod:: passlib.hash.nthash.raw_nthash(secret, hex=False) +The digest is calculated by encoding the secret using ``UTF-16-LE``, +taking the :mod:`~passlib.utils.md4` digest, and then encoding +that as hexidecimal. - perform raw nthash calculation, returning either - raw digest, or as lower-case hexidecimal characters. +FreeBSD Variant +=============== +For cross-compatibility, FreeBSD's :func:`!crypt` supports storing +NTHASH digests in a manner compatible with the :ref:`modular-crypt-format`, +to enable administrators to store user passwords in a manner compatible with +the SMB/CIFS protocol. This is accomplished by assigning NTHASH digests the +identifier ``$3$``, and prepending the identifier to the normal (lowercase) +NTHASH digest. An example digest (of ``password``) is +``$3$$8846f7eaee8fb117ad06bdd830b7586c`` (note the doubled ``$$``). + +.. data:: bsd_nthash + + This object supports FreeBSD's representation of NTHASH + (which is compatible with the :ref:`modular-crypt-format`), + and follows the :ref:`password-hash-api`. -Format & Algorithm -================== -A nthash encoded for crypt consists of :samp:`$3$${checksum}` or -:samp:`$NT${checksum}`; where :samp:`{checksum}` is 32 hexidecimal digits -encoding the checksum. An example hash (of ``password``) is ``$3$$8846f7eaee8fb117ad06bdd830b7586c``. + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. -The checksum is simply the :mod:`~passlib.utils.md4` digest -of the secret using the ``UTF16-LE`` encoding, encoded in hexidecimal. + .. versionchanged:: 1.6 + This hash was named ``nthash`` under previous releases of Passlib. Security Issues =============== -This algorithm should be considered *completely* broken: rainbow tables -exist for quickly reversing this hash. +This algorithm should be considered *completely* broken: + +* It has no salt. +* The MD4 message digest has been severely compromised by collision and + preimage attacks. +* Brute-force and pre-computed attacks exist targeting MD4 hashes in general, + and the encoding used by NTHASH in particular. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.oracle10.rst passlib-1.6.1/docs/lib/passlib.hash.oracle10.rst --- passlib-1.5.3/docs/lib/passlib.hash.oracle10.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.oracle10.rst 2012-08-01 17:05:50.000000000 +0000 @@ -4,46 +4,42 @@ .. currentmodule:: passlib.hash -This class implements the hash algorithm used by the Oracle Database up to -version 10g Rel.2. It was superceded by a newer algorithm in :class:`Oracle 11 `. - .. warning:: This hash is not secure, and should not be used for any purposes besides manipulating existing Oracle 10 password hashes. -.. warning:: - - This implementation has not been compared - very carefully against the official implementation or reference documentation, - and it's behavior may not match under various border cases. - It should not be relied on for anything but novelty purposes - for the time being. - -Usage -===== +This class implements the hash algorithm used by the Oracle Database up to +version 10g Rel.2. It was superceded by a newer algorithm in :class:`Oracle 11 `. This class can be used directly as follows (note that this class requires a username for all encrypt/verify operations):: - >>> from passlib.hash import oracle10 as or10 + >>> from passlib.hash import oracle10 as oracle10 - >>> #encrypt password using specified username - >>> h = or10.encrypt("password", "username") - >>> h + >>> # encrypt password using specified username + >>> hash = oracle10.encrypt("password", user="username") + >>> hash '872805F3F4C83365' - >>> or10.identify(h) #check if hash is recognized + >>> # verify correct password + >>> oracle10.verify("password", hash, user="username") True - >>> or10.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if some other hash is recognized + >>> # verify correct password w/ wrong username + >>> oracle10.verify("password", hash, user="somebody") False - - >>> or10.verify("password", h, "username") #verify correct password - True - >>> or10.verify("password", h, "somebody") #verify correct password w/ wrong username - False - >>> or10.verify("password", h, "username") #verify incorrect password + >>> # verify incorrect password + >>> oracle10.verify("letmein", hash, user="username") False +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +.. warning:: + + This implementation has not been compared + very carefully against the official implementation or reference documentation, + and it's behavior may not match under various border cases. + *caveat emptor*. + Interface ========= .. autoclass:: oracle10() @@ -79,18 +75,19 @@ Oracle10 account passwords, due to the following flaws [#flaws]_: * It's use of the username as a salt value means that common usernames - (eg ``system``) will occur more frequently as salts, + (e.g. ``system``) will occur more frequently as salts, weakening the effectiveness of the salt in foiling pre-computed tables. -* The fact that is it case insensitive, and simply concatenates the username - and password, greatly reduces the keyspace for brute-force - or pre-computed attacks. +* The fact that it is case insensitive, and simply concatenates the username + and password, greatly reduces the keyspace that must be searched by + brute-force or pre-computed attacks. -* It's simplicity makes high-speed brute force attacks much more feasible. +* It's simplicity, and decades of research on high-speed DES + implementations, makes efficient brute force attacks much more feasible. Deviations ========== -PassLib's implementation of the Oracle10g hash may deviate from the official +Passlib's implementation of the Oracle10g hash may deviate from the official implementation in unknown ways, as there is no official documentation. There is only one known issue: @@ -99,7 +96,7 @@ Lack of testing (and test vectors) leaves it unclear as to how Oracle 10g handles passwords containing non-7bit ascii. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-16-be`` [#enc]_ + Passlib will encode unicode passwords using ``utf-16-be`` [#enc]_ before running them through the Oracle10g algorithm. This behavior may be altered in the future, if further testing reveals another behavior is more in line with the official representation. @@ -109,7 +106,7 @@ .. rubric:: Footnotes .. [#enc] The exact encoding used in step 3 of the algorithm is not clear from known references. - PassLib uses ``utf-16-be``, as this is both compatible with existing test vectors + Passlib uses ``utf-16-be``, as this is both compatible with existing test vectors, and supports unicode input. .. [#flaws] Whitepaper analyzing flaws in this algorithm - @@ -117,4 +114,3 @@ .. [#] Description of Oracle10g and Oracle11g algorithms - ``_. - diff -Nru passlib-1.5.3/docs/lib/passlib.hash.oracle11.rst passlib-1.6.1/docs/lib/passlib.hash.oracle11.rst --- passlib-1.5.3/docs/lib/passlib.hash.oracle11.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.oracle11.rst 2012-08-01 17:05:50.000000000 +0000 @@ -6,39 +6,33 @@ This class implements the hash algorithm introduced in version 11g of the Oracle Database. It supercedes the :class:`Oracle 10 ` password hash. +This class can be can be used directly as follows:: -.. warning:: + >>> from passlib.hash import oracle11 as oracle11 - This implementation has not been compared - very carefully against the official implementation or reference documentation, - and it's behavior may not match under various border cases. - It should not be relied on for anything but novelty purposes - for the time being. - -Usage -===== -PassLib provides an oracle11 class, which can be can be used directly as follows:: - - >>> from passlib.hash import oracle11 as or11 - - >>> #generate new salt, encrypt password - >>> h = or11.encrypt("password") - >>> h + >>> # generate new salt, encrypt password + >>> hash = oracle11.encrypt("password") + >>> hash 'S:4143053633E59B4992A8EA17D2FF542C9EDEB335C886EED9C80450C1B4E6' - >>> or11.identify(h) #check if hash is recognized + >>> # verify password + >>> oracle11.verify("password", hash) True - >>> or11.identify('JQMuyS6H.AGMo') #check if some other hash is recognized + >>> oracle11.verify("secret", hash) False - >>> or11.verify("password", h) #verify correct password - True - >>> or11.verify("secret", h) #verify incorrect password - False +.. seealso:: the generic :ref:`PasswordHash usage examples ` + +.. warning:: + + This implementation has not been compared + very carefully against the official implementation or reference documentation, + and it's behavior may not match under various border cases. + *caveat emptor*. Interface ========= -.. autoclass:: oracle11(checksum=None, salt=None, strict=False) +.. autoclass:: oracle11() Format & Algorithm ================== @@ -64,7 +58,7 @@ Deviations ========== -PassLib's implementation of the Oracle11g hash may deviate from the official +Passlib's implementation of the Oracle11g hash may deviate from the official implementation in unknown ways, as there is no official documentation. There is only one known issue: @@ -73,7 +67,7 @@ Lack of testing (and test vectors) leaves it unclear as to how Oracle 11g handles passwords containing non-7bit ascii. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through Oracle11. This behavior may be altered in the future, if further testing reveals another behavior is more in line with the official representation. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.pbkdf2_digest.rst passlib-1.6.1/docs/lib/passlib.hash.pbkdf2_digest.rst --- passlib-1.5.3/docs/lib/passlib.hash.pbkdf2_digest.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.pbkdf2_digest.rst 2012-07-11 17:32:51.000000000 +0000 @@ -6,10 +6,12 @@ .. currentmodule:: passlib.hash -PassLib provides three custom hash schemes based on the PBKDF2 [#pbkdf2]_ algorithm +Passlib provides three custom hash schemes based on the PBKDF2 [#pbkdf2]_ algorithm which are compatible with the :ref:`modular crypt format `: -:class:`!pbkdf2_sha1`, :class:`!pbkdf2_sha256`, :class:`!pbkdf2_sha512`. -They feature variable length salts, variable rounds. + +* :class:`pbkdf2_sha1` +* :class:`pbkdf2_sha256` +* :class:`pbkdf2_sha512` Security-wise, PBKDF2 is currently one of the leading key derivation functions, and has no known security issues. @@ -20,52 +22,45 @@ PBKDF2-SHA512 is one of the three hashes Passlib :ref:`recommends ` for new applications. -.. seealso:: - - Alternate version of these hashes - :doc:`LDAP-Compatible Simple PBKDF2 Hashes ` - -Usage -===== -All of the following classes can be used directly as follows:: +All of these classes can be used directly as follows:: - >>> from passlib.hash import pbkdf2_sha256 as engine + >>> from passlib.hash import pbkdf2_sha256 - >>> #generate new salt, encrypt password - >>> hash = engine.encrypt("password") + >>> # generate new salt, encrypt password + >>> hash = pbkdf2_sha256.encrypt("password") >>> hash '$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M' - >>> #same, but with explicit number of rounds and salt length - >>> engine.encrypt("password", rounds=8000, salt_size=10) + >>> # same, but with an explicit number of rounds and salt length + >>> pbkdf2_sha256.encrypt("password", rounds=8000, salt_size=10) '$pbkdf2-sha256$8000$XAuBMIYQQogxRg$tRRlz8hYn63B9LYiCd6PRo6FMiunY9ozmMMI3srxeRE' - >>> #check if hash is a pbkdf2-sha256 hash - >>> engine.identify(hash) + >>> # verify the password + >>> pbkdf2_sha256.verify("password", hash) True - >>> #check if some other hash is recognized - >>> engine.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') + >>> pbkdf2_sha256.verify("wrong", hash) False - >>> #verify correct password - >>> engine.verify("password", hash) - True - >>> #verify incorrect password - >>> engine.verify("wrong", hash) - False +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`ldap_pbkdf2_{digest} ` -- + alternate LDAP-compatible versions of these hashes. Interface ========= -.. class:: pbkdf2_sha1() +.. autoclass:: pbkdf2_sha256() + +.. class:: pbkdf2_sha512() except for the choice of message digest, - this class is the same as :class:`pbkdf2_sha512`. + this class is the same as :class:`pbkdf2_sha256`. -.. class:: pbkdf2_sha256() +.. class:: pbkdf2_sha1() except for the choice of message digest, - this class is the same as :class:`pbkdf2_sha512`. - -.. autoclass:: pbkdf2_sha512() + this class is the same as :class:`pbkdf2_sha256`. .. _mcf-pbkdf2-format: @@ -78,7 +73,7 @@ All of the pbkdf2 hashes defined by passlib follow the same format, :samp:`$pbkdf2-{digest}${rounds}${salt}${checksum}`. -* :samp:`$pbkdf2-{digest}$`` is used as the :ref:`modular-crypt-format` identifier +* :samp:`$pbkdf2-{digest}$` is used as the :ref:`modular-crypt-format` identifier (``$pbkdf2-sha256$`` in the example). * :samp:`{digest}` - this specifies the particular cryptographic hash @@ -89,22 +84,22 @@ this is encoded as a positive decimal number with no zero-padding (``6400`` in the example). -* :samp:`{salt}` - this is the :func:`adapted base64 encoding ` +* :samp:`{salt}` - this is the :func:`adapted base64 encoding ` of the raw salt bytes passed into the PBKDF2 function. -* :samp:`{checksum}` - this is the :func:`adapted base64 encoding ` +* :samp:`{checksum}` - this is the :func:`adapted base64 encoding ` of the raw derived key bytes returned from the PBKDF2 function. - Each scheme uses output size of it's specific :samp:`{digest}` + Each scheme uses the digest size of it's specific hash algorithm (:samp:`{digest}`) as the size of the raw derived key. This is enlarged by appromixately 4/3 by the base64 encoding, - resulting in a checksum size of 27, 43, and 86 for each of the respective algorithms. + resulting in a checksum size of 27, 43, and 86 for each of the respective algorithms listed above. The algorithm used by all of these schemes is deliberately identical and simple: The password is encoded into UTF-8 if not already encoded, -and passed through :func:`~passlib.utils.pbkdf2.pbkdf2` +and run through :func:`~passlib.utils.pbkdf2.pbkdf2` along with the decoded salt, the number of rounds, and a prf built from HMAC + the respective message digest. -The result is then encoded using :func:`~passlib.utils.adapted_b64_encode`. +The result is then encoded using :func:`~passlib.utils.ab64_encode`. .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.phpass.rst passlib-1.6.1/docs/lib/passlib.hash.phpass.rst --- passlib-1.5.3/docs/lib/passlib.hash.phpass.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.phpass.rst 2012-05-03 16:36:58.000000000 +0000 @@ -1,4 +1,4 @@ -.. index:: phpass; portable hash, phpbb3; phpass hash +.. index:: PHPass; portable hash, phpBB3; PHPass hash ================================================================== :class:`passlib.hash.phpass` - PHPass' Portable Hash @@ -7,21 +7,20 @@ .. currentmodule:: passlib.hash This algorithm is used primarily by PHP software -which uses PHPass [#home], -a PHP library similar to PassLib. The PHPass Portable Hash +which uses PHPass [#pp]_, +a PHP library similar to Passlib. The PHPass Portable Hash is a custom password hash used by PHPass as a fallback when none of it's other hashes are available. Due to it's reliance on MD5, and the simplistic implementation, other hash algorithms should be used if possible. -Usage -===== -Supporting a variable sized salt and variable number of rounds, -this scheme is used in exactly the same way as :doc:`bcrypt `. +.. seealso:: + :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. Interface ========= -.. autoclass:: phpass(checksum=None, salt=None, rounds=None, strict=False) +.. autoclass:: phpass() Format ================== @@ -66,10 +65,10 @@ is implied by nearly all known reference hashes. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through phpass. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.plaintext.rst passlib-1.6.1/docs/lib/passlib.hash.plaintext.rst --- passlib-1.5.3/docs/lib/passlib.hash.plaintext.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.plaintext.rst 2012-05-03 16:36:58.000000000 +0000 @@ -4,37 +4,31 @@ .. currentmodule:: passlib.hash -This class stores passwords in plaintext. -This is, of course, ridiculously insecure; +This class stores passwords in plaintext. This is, of course, ridiculously insecure; it is provided for backwards compatibility when migrating existing applications. *It should not be used* for any other purpose. - -.. seealso:: - - * :class:`passlib.hash.ldap_plaintext` is probably more appropriate - to use in conjunction with other LDAP style hashes. - -Usage -===== -This class is mainly useful only for plugging into a :class:`~passlib.context.CryptContext`. -When used, it should always be the last scheme in the list, -as it will recognize all hashes. +This class should always be the last algorithm checked, as it will recognize all hashes. It can be used directly as follows:: - >>> from passlib.hash import plaintext as pt + >>> from passlib.hash import plaintext as plaintext - >>> #"encrypt" password - >>> pt.encrypt("password") + >>> # "encrypt" password + >>> plaintext.encrypt("password") 'password' - >>> nt.identify('password') #check if hash is recognized (all hashes are recognized) - True - - >>> nt.verify("password", "password") #verify correct password + >>> # verify password + >>> plaintext.verify("password", "password") True - >>> nt.verify("secret", "password") #verify incorrect password + >>> plaintext.verify("secret", "password") False +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :class:`ldap_plaintext ` -- on LDAP systems, + this format is probably more appropriate for storing plaintext passwords. + Interface ========= -.. autoclass:: plaintext +.. autoclass:: plaintext() diff -Nru passlib-1.5.3/docs/lib/passlib.hash.postgres_md5.rst passlib-1.6.1/docs/lib/passlib.hash.postgres_md5.rst --- passlib-1.5.3/docs/lib/passlib.hash.postgres_md5.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.postgres_md5.rst 2012-08-01 17:05:50.000000000 +0000 @@ -1,45 +1,42 @@ -.. index:: postgres; md5 hash +.. index:: Postgres; md5 hash ================================================================== :class:`passlib.hash.postgres_md5` - PostgreSQL MD5 password hash ================================================================== -.. currentmodule:: passlib.hash - -This class implements the md5-based hash algorithm used by PostgreSQL to store -it's user account passwords. This scheme was introduced in PostgreSQL 7.2; -prior to this PostgreSQL stored it's password in plain text. - .. warning:: This hash is not secure, and should not be used for any purposes besides manipulating existing PostgreSQL password hashes. -Usage -===== +.. currentmodule:: passlib.hash + +This class implements the md5-based hash algorithm used by PostgreSQL to store +it's user account passwords. This scheme was introduced in PostgreSQL 7.2; +prior to this PostgreSQL stored it's password in plain text. Users will most likely find the frontend provided by :mod:`passlib.apps` to be more useful than accessing this class directly. That aside, this class can be used directly as follows:: - >>> from passlib.hash import postgres_md5 as pm + >>> from passlib.hash import postgres_md5 - >>> #encrypt password using specified username - >>> h = pm.encrypt("password", "username") - >>> h + >>> # encrypt password using specified username + >>> hash = postgres_md5.encrypt("password", user="username") + >>> hash 'md55a231fcdb710d73268c4f44283487ba2' - >>> pm.identify(h) #check if hash is recognized + >>> # verify correct password + >>> postgres_md5.verify("password", hash, user="username") True - >>> pm.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') #check if some other hash is recognized + >>> # verify correct password w/ wrong username + >>> postgres_md5.verify("password", hash, user="somebody") False - - >>> pm.verify("password", h, "username") #verify correct password - True - >>> pm.verify("password", h, "somebody") #verify correct password w/ wrong username - False - >>> pm.verify("password", h, "username") #verify incorrect password + >>> # verify incorrect password + >>> postgres_md5.verify("password", hash, user="username") False +.. seealso:: the generic :ref:`PasswordHash usage examples ` + Interface ========= .. autoclass:: postgres_md5() @@ -56,7 +53,7 @@ PostgreSQL account passwords, due to the following flaws: * It's use of the username as a salt value means that common usernames - (eg ``admin``, ``root``, ``postgres``) will occur more frequently as salts, + (e.g. ``admin``, ``root``, ``postgres``) will occur more frequently as salts, weakening the effectiveness of the salt in foiling pre-computed tables. * Since the keyspace of ``user+password`` is still a subset of ascii characters, diff -Nru passlib-1.5.3/docs/lib/passlib.hash.rst passlib-1.6.1/docs/lib/passlib.hash.rst --- passlib-1.5.3/docs/lib/passlib.hash.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.rst 2012-05-03 16:36:58.000000000 +0000 @@ -3,37 +3,71 @@ ============================================== .. module:: passlib.hash - :synopsis: all password hashes provided by PassLib + :synopsis: all password hashes provided by Passlib Overview ======== -The :mod:`!passlib.hash` module contains all the password hashes built into Passlib. -Each object within this package implements a different password hashing scheme, -but all have the same uniform interface. The hashes in this module can used in two ways: +The :mod:`!passlib.hash` module contains all the password hash algorithms built into Passlib. +While each hash has it's own options and output format, they all share a common interface, +documented in detail in the :ref:`password-hash-api`. The following pages +first describe the common interface, and then each hash in detail ( +it's including format, algorithm, and known security issues). + +.. seealso:: :doc:`Quickstart Guide ` -- advice on + choosing an appropriately secure hash for your new application. + +Usage +===== +All of the hashes in this module can used in two ways: + +1. They can be imported and used directly, as in the following example + with the :class:`md5_crypt` hash:: + + >>> # import the desired hash + >>> from passlib.hash import md5_crypt + + >>> # hash the password - encrypt() takes care of salt generation, unicode encoding, etc. + >>> hash = md5_crypt.encrypt("password") + >>> hash + '$1$IU54yC7Y$nI1wF8ltcRvaRHwMIjiJq1' + + >>> # verify a password against an existing hash: + >>> md5_crypt.verify("password", hash) + True + +2. Alternately, when working with multiple algorithms at once, it is frequently useful + to construct a :ref:`CryptContext ` object instead; + and reference the hashes by name only. For example, the following + code creates a :class:`!CryptContext` object which recognizes both + the :class:`md5_crypt` and :class:`des_crypt` hash algorithms:: + + >>> # import and create the context object + >>> from passlib.context import CryptContext + >>> pwd_context = CryptContext(schemes=["md5_crypt", "des_crypt"]) + + >>> # hash two different passwords (context objects used the first scheme as the default) + >>> hash1 = pwd_context.encrypt("password") + >>> hash1 + '$1$2y72Yi12$o6Yu2OyjN.9FiK.9HJ7i5.' + >>> hash2 = pwd_context.encrypt("letmein", scheme="des_crypt") + >>> hash2 + '0WMdk/ven8bok' + + >>> # the context object takes care of figuring out which hash belongs to which algorithm. + >>> pwd_context.verify("password", hash1) + True + >>> pwd_context.verify("letmein", hash1) + False + >>> pwd_context.verify("letmein", hash2) + True -They can be imported and used directly, as in the following example:: +For additional details, usage examples, and full documentation of all +methods and attributes provided by the common hash interface: - >>> from passlib.hash import md5_crypt - >>> md5_crypt.encrypt("password") - '$1$IU54yC7Y$nI1wF8ltcRvaRHwMIjiJq1' - -More commonly, they can be referenced by name -when constructing a custom :doc:`CryptContext ` object, -as in the following example:: - - >>> from passlib.context import CryptContext - >>> #note below that md5_crypt and des_crypt are both names of classes in passlib.hash - >>> pwd_context = CryptContext(["md5_crypt", "des_crypt"]) - >>> pwd_context.encrypt("password") - '$1$2y72Yi12$o6Yu2OyjN.9FiK.9HJ7i5.' - -.. seealso:: - - * :ref:`password-hash-api` -- details the - interface used by all password hashes in this module. +.. toctree:: + :maxdepth: 2 - * :doc:`Quickstart Guide ` -- - for advice on choosing an appropriately secure hash for your new application. + /password_hash_api .. _mcf-hashes: @@ -90,10 +124,12 @@ passlib.hash.apr_md5_crypt passlib.hash.phpass - passlib.hash.nthash passlib.hash.pbkdf2_digest passlib.hash.cta_pbkdf2_sha1 passlib.hash.dlitz_pbkdf2_sha1 + passlib.hash.scram + +* :class:`passlib.hash.bsd_nthash` - FreeBSD's MCF-compatible :doc:`nthash ` encoding Special note should be made of the fallback helper, which is not an actual hash scheme, but provides "disabled account" @@ -102,7 +138,7 @@ .. toctree:: :maxdepth: 1 - passlib.hash.unix_fallback + passlib.hash.unix_disabled .. _ldap-hashes: @@ -113,6 +149,9 @@ used by LDAPv2. Originally specified in :rfc:`2307` and used by OpenLDAP [#openldap]_, the basic format ``{SCHEME}HASH`` has seen widespread adoption in a number of programs. +.. [#openldap] OpenLDAP homepage - ``_. + + .. _standard-ldap-hashes: Standard LDAP Schemes @@ -161,8 +200,8 @@ .. _database-hashes: -Database Hashes -=============== +SQL Database Hashes +=================== The following schemes are used by various SQL databases to encode their own user accounts. These schemes have encoding and contextual requirements @@ -171,12 +210,30 @@ .. toctree:: :maxdepth: 1 + passlib.hash.mssql2000 + passlib.hash.mssql2005 passlib.hash.mysql323 passlib.hash.mysql41 passlib.hash.postgres_md5 passlib.hash.oracle10 passlib.hash.oracle11 +.. _windows-hashes: + +MS Windows Hashes +================= +The following hashes are used in various places by Microsoft Windows. +As they were designed for "internal" use, they generally contain +no identifying markers, identifying them is pretty much context-dependant. + +.. toctree:: + :maxdepth: 1 + + passlib.hash.lmhash + passlib.hash.nthash + passlib.hash.msdcc + passlib.hash.msdcc2 + .. _other-hashes: Other Hashes @@ -188,11 +245,15 @@ .. toctree:: :maxdepth: 1 + passlib.hash.cisco_pix + +* *Cisco "Type 5" hashes* - see :doc:`md5_crypt ` + +.. toctree:: + :maxdepth: 1 + + passlib.hash.cisco_type7 passlib.hash.django_std passlib.hash.grub_pbkdf2_sha512 passlib.hash.hex_digests passlib.hash.plaintext - -.. rubric:: Footnotes - -.. [#openldap] OpenLDAP homepage - ``_. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.scram.rst passlib-1.6.1/docs/lib/passlib.hash.scram.rst --- passlib-1.5.3/docs/lib/passlib.hash.scram.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.scram.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,171 @@ +.. index:: SCRAM protocol + +=================================================================== +:class:`passlib.hash.scram` - SCRAM Hash +=================================================================== + +.. versionadded:: 1.6 + +.. currentmodule:: passlib.hash + +SCRAM is a password-based challenge response protocol defined by :rfc:`5802`. +While Passlib does not provide an implementation of SCRAM, applications +which use SCRAM on the server side frequently need a way to store +user passwords in a secure format that can be used to authenticate users over +SCRAM. + +To accomplish this, Passlib provides the following +:ref:`modular-crypt-format`-compatible password hash scheme which uses the +``$scram$`` identifier. This format encodes a salt, rounds settings, and one +or more :func:`~passlib.utils.pbkdf2.pbkdf2` digests... one digest for each +of the hash algorithms the server wishes to support over SCRAM. + +Since this format is PBKDF2-based, it has equivalent security to +Passlib's other :doc:`pbkdf2 hashes `, +and can be used to authenticate users using either the normal :ref:`password-hash-api` +or the SCRAM-specific class methods documentated below. + +.. note:: + + If you aren't working with the SCRAM protocol, you probably + don't need to use this hash format. + +Usage +===== +This class can be used like any other Passlib hash, as follows:: + + >>> from passlib.hash import scram + + >>> # generate new salt, encrypt password against default list of algorithms + >>> hash = scram.encrypt("password") + >>> hash + '$scram$6400$.Z/znnNOKWUsBaCU$sha-1=cRseQyJpnuPGn3e6d6u6JdJWk.0,sha-256=5G + cjEbRaUIIci1r6NAMdI9OPZbxl9S5CFR6la9CHXYc,sha-512=.DHbIm82ajXbFR196Y.9Ttbs + gzvGjbMeuWCtKve8TPjRMNoZK9EGyHQ6y0lW9OtWdHZrDZbBUhB9ou./VI2mlw' + + >>> # same, but with an explicit number of rounds + >>> scram.encrypt("password", rounds=8000) + '$scram$8000$Y0zp/R/DeO89h/De$sha-1=eE8dq1f1P1hZm21lfzsr3CMbiEA,sha-256=Nf + kaDFMzn/yHr/HTv7KEFZqaONo6psRu5LBBFLEbZ.o,sha-512=XnGG11X.J2VGSG1qTbkR3FVr + 9j5JwsnV5Fd094uuC.GtVDE087m8e7rGoiVEgXnduL48B2fPsUD9grBjURjkiA' + + >>> # verify password + >>> scram.verify("password", hash) + True + >>> scram.verify("secret", hash) + False + +See the generic :ref:`PasswordHash usage examples ` +for more details on how to use the common hash interface. + +---- + +Additionally, this class provides a number of useful methods for SCRAM-specific actions: + +* You can override the default list of digests, and/or the number of iterations:: + + >>> hash = scram.encrypt("password", rounds=1000, algs="sha-1,sha-256,md5") + >>> hash + '$scram$1000$RsgZo7T2/l8rBUBI$md5=iKsH555d3ctn795Za4S7bQ,sha-1=dRcE2AUjALLF + tX5DstdLCXZ9Afw,sha-256=WYE/LF7OntriUUdFXIrYE19OY2yL0N5qsQmdPNFn7JE' + +* Given a scram hash, you can use a single call to extract all the information + the SCRAM needs to authenticate against a specific mechanism:: + + >>> # this returns (salt_bytes, rounds, digest_bytes) + >>> scram.extact_digest_info(hash, "sha-1") + ('F\xc8\x19\xa3\xb4\xf6\xfe_+\x05@H', + 1000, + 'u\x17\x04\xd8\x05#\x00\xb2\xc5\xb5~C\xb2\xd7K\tv}\x01\xfc') + +* Given a scram hash, you can extract the list of digest algorithms + it contains information for (``sha-1`` will always be present):: + + >>> scram.extract_digest_algs(hash) + ["md5", "sha-1", "sha-256"] + +* This class also provides a standalone helper which can calculate + the ``SaltedPassword`` portion of the SCRAM protocol, taking + care of the SASLPrep step as well:: + + >>> scram.derive_digest("password", b'\x01\x02\x03', 1000, "sha-1") + b'k\x086vg\xb3\xfciz\xb4\xb4\xe2JRZ\xaet\xe4`\xe7' + +Interface +========= +.. note:: + + This hash format is new in Passlib 1.6, and it's SCRAM-specific API + may change in the next few releases, depending on user feedback. + +.. autoclass:: scram() + +.. rst-class:: html-toggle + +Format & Algorithm +================== +An example scram hash (of the string ``password``) is:: + + $scram$6400$.Z/znnNOKWUsBaCU$sha-1=cRseQyJpnuPGn3e6d6u6JdJWk.0,sha-256=5G + cjEbRaUIIci1r6NAMdI9OPZbxl9S5CFR6la9CHXYc,sha-512=.DHbIm82ajXbFR196Y.9Ttb + sgzvGjbMeuWCtKve8TPjRMNoZK9EGyHQ6y0lW9OtWdHZrDZbBUhB9ou./VI2mlw + +An scram hash string has the format :samp:`$scram${rounds}${salt}${alg1}={digest1},{alg2}={digest2},...`, where: + +* ``$scram$`` is the prefix used to identify Passlib scram hashes, + following the :ref:`modular-crypt-format` + +* :samp:`{rounds}` is the number of decimal rounds to use (6400 in the example), + zero-padding not allowed. this value must be in ``range(1, 2**32)``. + +* :samp:`{salt}` is a base64 salt string (``.Z/znnNOKWUsBaCU`` in the example), + encoded using :func:`~passlib.utils.ab64_encode`. + +* :samp:`{alg}` is a lowercase IANA hash function name [#hnames]_, which should + match the digest in the SCRAM mechanism name. + +* :samp:`{digest}` is a base64 digest for the specific algorithm, + encoded using :func:`~passlib.utils.ab64_encode`. + Digests for ``sha-1``, ``sha-256``, and ``sha-512`` are present in the example. + +* There will always be one or more :samp:`{alg}={digest}` pairs, separated by a + comma. Per the SCRAM specification, the algorithm ``sha-1`` should always be present. + +There is also an alternate format (:samp:`$scram${rounds}${salt}${alg},...`) +which is used to represent a configuration string that doesn't contain +any digests. An example would be:: + + $scram$6400$.Z/znnNOKWUsBaCU$sha-1,sha-256,sha-512 + +The algorithm used to calculate each digest is:: + + pbkdf2(salsprep(password).encode("utf-8"), salt, rounds, alg_digest_size, "hmac-"+alg) + +...as laid out in the SCRAM specification [#scram]_. All digests +should verify against the same password, or the hash is considered malformed. + +.. note:: + + This format is similar in spirit to the LDAP storage format for SCRAM hashes, + defined in :rfc:`5803`, except that it encodes everything into a single + string, and does not have any storage requirements (outside of the ability + to store 512+ character ascii strings). + +Security +======== +The security of this hash is only as strong as the weakest digest used +by this hash. Since the SCRAM [#scram]_ protocol requires SHA1 +always be supported, this will generally be the weakest link, since +the other digests will generally be stronger ones (e.g. SHA2-256). + +None-the-less, since PBKDF2 is sufficiently collision-resistant +on it's own, any pre-image weakenesses found in SHA1 should be mitigated +by the PBKDF2-HMAC-SHA1 wrapper; and should have no flaws outside of +brute-force attacks on PBKDF2-HMAC-SHA1. + +.. rubric:: Footnotes + +.. [#scram] The SCRAM protocol is laid out in :rfc:`5802`. + +.. [#hnames] The official list of IANA-assigned hash function names - + ``_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.sha1_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.sha1_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.sha1_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.sha1_crypt.rst 2012-05-03 16:36:58.000000000 +0000 @@ -8,14 +8,22 @@ It's based on a variation of the PBKDF1 algorithm, and supports a large salt and variable number of rounds. -Usage -===== -Supporting a variable sized salt and variable number of rounds, -this scheme is used in exactly the same way as :doc:`sha512_crypt `. +.. seealso:: + :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. -Functions +Interface ========= -.. autoclass:: sha1_crypt(checksum=None, salt=None, rounds=None, strict=False) +.. autoclass:: sha1_crypt() + +.. note:: + + This class will use the first available of two possible backends: + + * stdlib :func:`crypt()`, if the host OS supports sha1-crypt (NetBSD). + * a pure python implementation of sha1-crypt built into Passlib. + + You can see which backend is in use by calling the :meth:`get_backend()` method. Format ====== @@ -62,7 +70,7 @@ The NetBSD implementation randomly varies the actual number of rounds when generating a new configuration string, in order to decrease - predictability. This feature is provided by PassLib to *all* hashes, + predictability. This feature is provided by Passlib to *all* hashes, via the :class:`CryptContext` class, and so it omitted from this implementation. @@ -72,15 +80,15 @@ within the rounds portion of the hash. No existing examples or test vectors have zero padding, and allowing it would result in multiple encodings for the same configuration / hash. - To prevent this situation, PassLib will throw an error if the rounds in a hash + To prevent this situation, Passlib will throw an error if the rounds in a hash have leading zeros. * Restricted salt string character set: The underlying algorithm can unambigously handle salt strings which contain any possible byte value besides ``\x00`` and ``$``. - However, PassLib strictly limits salts to the - :mod:`hash 64 ` character set, + However, Passlib strictly limits salts to the + :data:`hash64 ` character set, as nearly all implementations of sha1-crypt generate and expect salts containing those characters. @@ -92,10 +100,10 @@ is implied by nearly all known reference hashes. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through sha1-crypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. .. rubric:: Footnotes diff -Nru passlib-1.5.3/docs/lib/passlib.hash.sha256_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.sha256_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.sha256_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.sha256_crypt.rst 2012-05-03 16:36:58.000000000 +0000 @@ -4,29 +4,119 @@ .. currentmodule:: passlib.hash -Defined by the same specification as :class:`~passlib.hash.sha512_crypt`, -SHA256-Crypt is identical to SHA512-Crypt in almost every way, including -design and security issues. It's main advantage over SHA512-Crypt is -that it may be faster on 32 bit operating systems. - -.. seealso:: :doc:`SHA512-Crypt ` - -Usage -===== -This class can be used in exactly the same manner as :class:`~passlib.hash.sha512_crypt`. +SHA-256 Crypt and SHA-512 Crypt were developed in 2008 by Ulrich Drepper [#f1]_, +designed as the successor to :class:`~passlib.hash.md5_crypt`. +They include fixes and advancements such as variable rounds, and use of NIST-approved cryptographic primitives. +The design involves repeated composition of the underlying digest algorithm, +using various arbitrary permutations of inputs. +SHA-512 / SHA-256 Crypt are currently the default password hash for many systems +(notably Linux), and have no known weaknesses. +SHA-256 Crypt is one of the three hashes Passlib :ref:`recommends ` +for new applications. +This class can be used directly as follows:: + + >>> from passlib.hash import sha256_crypt + + >>> # generate new salt, encrypt password + >>> hash = sha256_crypt.encrypt("password") + >>> hash + '$5$rounds=80000$wnsT7Yr92oJoP28r$cKhJImk5mfuSKV9b3mumNzlbstFUplKtQXXMo4G6Ep5' + + >>> # same, but with explict number of rounds + >>> sha256_crypt.encrypt("password", rounds=12345) + '$5$rounds=12345$q3hvJE5mn5jKRsW.$BbbYTFiaImz9rTy03GGi.Jf9YY5bmxN0LU3p3uI1iUB' + + >>> # verify password + >>> sha256_crypt.verify("password", hash) + True + >>> sha256_crypt.verify("letmein", hash) + False + +.. seealso:: + + * :ref:`password hash usage ` -- for more usage examples + + * :doc:`sha512_crypt ` -- the companion 512-bit version of this hash. Interface ========= -.. autoclass:: sha256_crypt(checksum=None, salt=None, rounds=None, strict=False) +.. autoclass:: sha256_crypt() + +.. note:: + + This class will use the first available of two possible backends: + + * stdlib :func:`crypt()`, if the host OS supports SHA256-Crypt (most Linux systems). + * a pure python implementation of SHA256-Crypt built into Passlib. + + You can see which backend is in use by calling the :meth:`get_backend()` method. Format & Algorithm ================== -SHA256-Crypt is defined by the same specification as SHA512-Crypt. -The format and algorithm are exactly the same, except for -the following notable differences: - -* it uses the :ref:`modular crypt prefix ` ``$5$``, whereas SHA-512-Crypt uses ``$6$``. -* it uses the SHA-256 message digest in place of the SHA-512 message digest. -* it's output hash is correspondingly smaller in size, encoding a 256 bit checksum instead of 512. +An example sha256-crypt hash (of the string ``password``) is: + + ``$5$rounds=80000$wnsT7Yr92oJoP28r$cKhJImk5mfuSKV9b3mumNzlbstFUplKtQXXMo4G6Ep5`` + +An sha256-crypt hash string has the format :samp:`$5$rounds={rounds}${salt}${checksum}`, where: + +* ``$5$`` is the prefix used to identify sha256-crypt hashes, + following the :ref:`modular-crypt-format` + +* :samp:`{rounds}` is the decimal number of rounds to use (80000 in the example). + +* :samp:`{salt}` is 0-16 characters drawn from ``[./0-9A-Za-z]``, providing a + 96-bit salt (``wnsT7Yr92oJoP28r`` in the example). + +* :samp:`{checksum}` is 43 characters drawn from the same set, encoding a 256-bit + checksum (``cKhJImk5mfuSKV9b3mumNzlbstFUplKtQXXMo4G6Ep5`` in the example). + +There is also an alternate format :samp:`$5${salt}${checksum}`, +which can be used when the rounds parameter is equal to 5000 +(see the ``implicit_rounds`` parameter above). + +The algorithm used by SHA256-Crypt is laid out in detail +in the specification document linked to below [#f1]_. + +Deviations +========== +This implementation of sha256-crypt differs from the specification, +and other implementations, in a few ways: + +* Zero-Padded Rounds: + + The specification does not specify how to deal with zero-padding + within the rounds portion of the hash. No existing examples + or test vectors have zero padding, and allowing it would + result in multiple encodings for the same configuration / hash. + To prevent this situation, Passlib will throw an error if the rounds + parameter in a hash has leading zeros. + +* Restricted salt string character set: + + The underlying algorithm can unambigously handle salt strings + which contain any possible byte value besides ``\x00`` and ``$``. + However, Passlib strictly limits salts to the + :data:`hash64 ` character set, + as nearly all implementations of sha256-crypt generate + and expect salts containing those characters, + but may have unexpected behaviors for other character values. + +* Unicode Policy: + + The underlying algorithm takes in a password specified + as a series of non-null bytes, and does not specify what encoding + should be used; though a ``us-ascii`` compatible encoding + is implied by nearly all implementations of sha256-crypt + as well as all known reference hashes. + + In order to provide support for unicode strings, + Passlib will encode unicode passwords using ``utf-8`` + before running them through sha256-crypt. If a different + encoding is desired by an application, the password should be encoded + before handing it to Passlib. + +.. rubric:: Footnotes -See SHA512-Crypt for more details. +.. [#f1] Ulrich Drepper's SHA-256/512-Crypt specification, reference + implementation, and test vectors - + `sha-crypt specification `_ diff -Nru passlib-1.5.3/docs/lib/passlib.hash.sha512_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.sha512_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.sha512_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.sha512_crypt.rst 2012-05-03 16:36:58.000000000 +0000 @@ -4,112 +4,43 @@ .. currentmodule:: passlib.hash -SHA-512 Crypt and SHA-256 Crypt were developed in 2008 by Ulrich Drepper [#f1]_ -as a successor to :class:`~passlib.hash.md5_crypt`. They include fixes -and advancements such as variable rounds, and use of NIST-approved cryptographic primitives. -SHA-256 / SHA-512 Crypt are currently the default password hash for many systems -(notably Linux), and have no known weaknesses. -SHA-512 Crypt is one of the three hashes Passlib :ref:`recommends ` -for new applications. - -Usage -===== -This class can be used directly as follows:: - - >>> from passlib.hash import sha512_crypt as sc - - >>> #generate new salt, encrypt password - >>> h = sc.encrypt("password") - >>> h - '$6$rounds=40000$xCsOXRqPPk5AGDFu$o5eyqxEoOSq0dLRFbPxEHp5Jc1vFVj47BNT.h9gmjSHXDS15mjIM.GSUaT5r6Z.Xa1Akrv4FAgKJE3EfbkJxs1' - - >>> #same, but with explict number of rounds - >>> sc.encrypt("password", rounds=10000) - '$6$rounds=10000$QWT8AlDMYRms7vSx$.1267Pg6Opn9CblFndtBJ2Q0AI0fcI2IX93zX3gi1Qse./j.VlKYX59NIUlbs0A66wCbfu/vra9wMv2uwTZAI.' - - >>> #check if hash is recognized - >>> sc.identify(h) - True - >>> #check if some other hash is recognized - >>> sc.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') - False - - >>> #verify correct password - >>> sc.verify("password", h) - True - >>> sc.verify("secret", h) #verify incorrect password - False +Defined by the same specification as :class:`~passlib.hash.sha256_crypt`, +SHA512-Crypt is identical to SHA256-Crypt in almost every way, including +design and security issues. The only difference is the doubled digest size; +while this provides some increase in security, it's also a bit slower 32 bit operating systems. -Interface -========= -.. autoclass:: sha512_crypt(checksum=None, salt=None, rounds=None, strict=False) - -Format & Algorithm -================== -An example sha512-crypt hash (of the string ``password``) is: - - ``$6$rounds=40000$JvTuqzqw9bQ8iBl6$SxklIkW4gz00LvuOsKRCfNEllLciOqY/FSAwODHon45YTJEozmy.QAWiyVpuiq7XMTUMWbIWWEuQytdHkigcN/``. - -An sha512-crypt hash string has the format :samp:`$6$rounds={rounds}${salt}${checksum}`, where: - -* ``$6$`` is the prefix used to identify sha512-crypt hashes, - following the :ref:`modular-crypt-format` - -* :samp:`{rounds}` is the decimal number of rounds to use (40000 in the example). - -* :samp:`{salt}` is 0-16 characters drawn from ``[./0-9A-Za-z]``, providing a - 96-bit salt (``JvTuqzqw9bQ8iBl6`` in the example). +.. seealso:: -* :samp:`{checksum}` is 86 characters drawn from the same set, encoding a 512-bit - checksum. + * :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. - (``SxklIkW4gz00LvuOsKRCfNEllLciOqY/FSAwODHon45YTJEozmy.QAWiyVpuiq7XMTUMWbIWWEuQytdHkigcN/`` in the example). + * :doc:`sha256_crypt ` -- the companion 256-bit version + of this hash. -There is also an alternate format :samp:`$6${salt}${checksum}`, -which can be used when the rounds parameter is equal to 5000 -(see the ``implicit_rounds`` parameter above). - -The algorithm used by SHA512-Crypt is laid out in detail -in the specification document linked to below [#f1]_. - -Deviations -========== -This implementation of sha512-crypt differs from the specification, -and other implementations, in a few ways: - -* Zero-Padded Rounds: - - The specification does not specify how to deal with zero-padding - within the rounds portion of the hash. No existing examples - or test vectors have zero padding, and allowing it would - result in multiple encodings for the same configuration / hash. - To prevent this situation, PassLib will throw an error if the rounds - parameter in a hash has leading zeros. - -* Restricted salt string character set: - - The underlying algorithm can unambigously handle salt strings - which contain any possible byte value besides ``\x00`` and ``$``. - However, PassLib strictly limits salts to the - :mod:`hash 64 ` character set, - as nearly all implementations of sha512-crypt generate - and expect salts containing those characters, - but may have unexpected behaviors for other character values. +Interface +========= +.. autoclass:: sha512_crypt() -* Unicode Policy: +.. note:: - The underlying algorithm takes in a password specified - as a series of non-null bytes, and does not specify what encoding - should be used; though a ``us-ascii`` compatible encoding - is implied by nearly all implementations of sha512-crypt - as well as all known reference hashes. + This class will use the first available of two possible backends: - In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` - before running them through sha512-crypt. If a different - encoding is desired by an application, the password should be encoded - before handing it to PassLib. + * stdlib :func:`crypt()`, if the host OS supports SHA512-Crypt (most Linux systems). + * a pure python implementation of SHA512-Crypt built into passlib. -.. rubric:: Footnotes + You can see which backend is in use by calling the :meth:`get_backend()` method. -.. [#f1] Ulrich Drepper's SHA-256/512-Crypt specification, reference implementation, and test vectors - `sha-crypt specification `_ +Format & Algorithm +================== +SHA512-Crypt is defined by the same specification as SHA256-Crypt. +The format and algorithm are exactly the same, except for +the following notable differences: + +* it uses the :ref:`modular crypt prefix ` ``$6$``, whereas SHA256-Crypt uses ``$5$``. +* it uses the SHA-512 message digest in place of the SHA-256 message digest. +* it's output hash is correspondingly larger in size, + with an 86-character encoded checksum, instead of 43 characters. + +See :doc:`sha256_crypt ` +for the format and algorithm descriptions, +as well as security notes. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.sun_md5_crypt.rst passlib-1.6.1/docs/lib/passlib.hash.sun_md5_crypt.rst --- passlib-1.5.3/docs/lib/passlib.hash.sun_md5_crypt.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.sun_md5_crypt.rst 2012-08-01 17:05:50.000000000 +0000 @@ -1,4 +1,4 @@ -.. index:: solaris; sun_md5_crypt +.. index:: Solaris; sun_md5_crypt ================================================================= :class:`passlib.hash.sun_md5_crypt` - Sun MD5 Crypt @@ -11,21 +11,20 @@ in common with the :class:`~passlib.hash.md5_crypt` algorithm. It supports 32 bit variable rounds and an 8 character salt. -.. warning:: +.. seealso:: + :ref:`password hash usage ` -- + for examples of how to use this class via the common hash interface. + +.. note:: The original Solaris implementation has some hash encoding quirks which may not be properly accounted for in Passlib. - For now, this implementation should not be relied on for anything but novelty purposes. - -Usage -===== -This class supports both rounds and salts, -and so can be used in the exact same manner -as :doc:`SHA-512 Crypt `. + Until more user feedback and sample hashes have been gathered, + *caveat emptor*. Interface ========= -.. autoclass:: sun_md5_crypt(checksum=None, salt=None, rounds=None, bare_salt=False, strict=False) +.. autoclass:: sun_md5_crypt() Format ====== @@ -135,7 +134,7 @@ implementation's parser. This bug causes the implementation to return ``$$``-format hashes when passed a configuration string that ends with ``$``. It returns the intended original format & checksum -only if there is at least one letter after the ``$``, eg :samp:`$md5${salt}$x`. +only if there is at least one letter after the ``$``, e.g. :samp:`$md5${salt}$x`. Passlib attempts to accomodate both formats using the special ``bare_salt`` keyword. It is set to ``True`` to indicate a configuration or hash string which @@ -147,7 +146,7 @@ Deviations ========== -PassLib's implementation of Sun-MD5-Crypt deliberately +Passlib's implementation of Sun-MD5-Crypt deliberately deviates from the official implementation in the following ways: * Unicode Policy: @@ -158,19 +157,19 @@ is implied by all known reference hashes. In order to provide support for unicode strings, - PassLib will encode unicode passwords using ``utf-8`` + Passlib will encode unicode passwords using ``utf-8`` before running them through sun-md5-crypt. If a different encoding is desired by an application, the password should be encoded - before handing it to PassLib. + before handing it to Passlib. * Rounds encoding - The underlying scheme implicitly allows rounds to have zero padding (eg ``$md5,rounds=001$abc$``), + The underlying scheme implicitly allows rounds to have zero padding (e.g. ``$md5,rounds=001$abc$``), and also allows 0 rounds to be specified two ways (``$md5$abc$`` and ``$md5,rounds=0$abc$``). Allowing either of these would result in multiple possible checksums for the same password & salt. To prevent ambiguity, Passlib will throw a :exc:`ValueError` if the rounds value is zero-padded, - or specified explicitly as 0 (eg ``$md5,rounds=0$abc$``). + or specified explicitly as 0 (e.g. ``$md5,rounds=0$abc$``). .. _smc-quirks: diff -Nru passlib-1.5.3/docs/lib/passlib.hash.unix_disabled.rst passlib-1.6.1/docs/lib/passlib.hash.unix_disabled.rst --- passlib-1.5.3/docs/lib/passlib.hash.unix_disabled.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.unix_disabled.rst 2012-05-08 14:22:31.000000000 +0000 @@ -0,0 +1,47 @@ +================================================================== +:class:`passlib.hash.unix_disabled` - Unix Disabled Account Helper +================================================================== + +.. currentmodule:: passlib.hash + +This class does not provide an encryption scheme, +but instead provides a helper for handling disabled +password fields as found in unix ``/etc/shadow`` files. +This class is mainly useful only for plugging into a +:class:`~passlib.context.CryptContext` instance. +It can be used directly as follows:: + + >>> from passlib.hash import unix_disabled + + >>> # 'encrypting' a password always results in "!" or "*" + >>> unix_disabled.encrypt("password") + '!' + + >>> # verifying will fail for all passwords and hashes + >>> unix_disabled.verify("password", "!") + False + >>> unix_disabled.verify("letmein", "*NOPASSWORD*") + False + + >>> # this class should identify all strings which aren't + >>> # valid Unix crypt() output, while leaving MCF hashes alone + >>> unix_disabled.identify('!') + True + >>> unix_disabled.identify('') + True + >>> unix_disabled.identify("$1$somehash") + False + +Interface +========= +.. autoclass:: unix_disabled() + +Deprecated Interface +==================== +.. autoclass:: unix_fallback() + +Deviations +========== +According to the Linux ``shadow`` man page, an empty string is treated +as a wildcard by Linux, allowing all passwords. For security purposes, +this behavior is NOT supported; empty strings are treated the same as ``!`` or ``*``. diff -Nru passlib-1.5.3/docs/lib/passlib.hash.unix_fallback.rst passlib-1.6.1/docs/lib/passlib.hash.unix_fallback.rst --- passlib-1.5.3/docs/lib/passlib.hash.unix_fallback.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hash.unix_fallback.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,52 +0,0 @@ -================================================================== -:class:`passlib.hash.unix_fallback` - Unix Fallback Helper -================================================================== - -.. currentmodule:: passlib.hash - -This class does not provide an encryption scheme, -but instead provides a helper for handling disabled / wildcard -password fields as found in unix ``/etc/shadow`` files. - -Usage -===== -This class is mainly useful only for plugging into a :class:`~passlib.context.CryptContext`. -When used, it should always be the last scheme in the list, -as it is designed to provide a fallback behavior. -It can be used directly as follows:: - - >>> from passlib.hash import unix_fallback as uf - - >>> #'encrypting' a password always results in "!", the default reject hash. - >>> uf.encrypt("password") - '!' - - >>> #check if hash is recognized (all strings are recognized) - >>> uf.identify('!') - True - >>> uf.identify('*') - True - >>> uf.identify('') - True - - >>> #verify against non-empty string - no passwords allowed - >>> uf.verify("password", "!") - False - - >>> #verify against empty string: - >>> # * by default, no passwords allowed - >>> # * all passwords allowed IF enable_wildcard=True - >>> uf.verify("password", "") - False - >>> uf.verify("password", "", enable_wildcard=True) - True - -Interface -========= -.. autoclass:: unix_fallback - -Deviations -========== -According to the Linux ``shadow`` man page, an empty string is treated -as a wildcard by Linux, allowing all passwords. For security purposes, -this behavior is not enabled unless specifically requested by the application. diff -Nru passlib-1.5.3/docs/lib/passlib.hosts.rst passlib-1.6.1/docs/lib/passlib.hosts.rst --- passlib-1.5.3/docs/lib/passlib.hosts.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.hosts.rst 2012-08-01 17:05:50.000000000 +0000 @@ -5,25 +5,68 @@ .. module:: passlib.hosts :synopsis: encrypting & verifying operating system passwords -This module provides :class:`!CryptContext` instances for encrypting & -verifying password hashes tied to user accounts of various operating systems. +This module provides some preconfigured :ref:`CryptContext ` +instances for encrypting & verifying password hashes tied to user accounts of various operating systems. While (most) of the objects are available cross-platform, their use is oriented primarily towards Linux and BSD variants. .. seealso:: + for Microsoft Windows, see the list of :ref:`windows-hashes` + in :mod:`passlib.hash`. - :mod:`passlib.context` module for details about how to use a :class:`!CryptContext` instance. +.. rst-class:: html-toggle + +Usage Example +============= +The :class:`!CryptContext` class itself has a large number of features, +but to give an example of how to quickly use the instances in this module: + +Each of the objects in this module can be imported directly:: + + >>> # as an example, this imports the linux_context object, + >>> # which is configured to recognized most hashes found in Linux /etc/shadow files. + >>> from passlib.apps import linux_context + +Encrypting a password is simple (and salt generation is handled automatically):: + + >>> hash = linux_context.encrypt("toomanysecrets") + >>> hash + '$5$rounds=84740$fYChCy.52EzebF51$9bnJrmTf2FESI93hgIBFF4qAfysQcKoB0veiI0ZeYU4' + +Verifying a password against an existing hash is just as quick:: + + >>> linux_context.verify("toomanysocks", hash) + False + >>> linux_context.verify("toomanysecrets", hash) + True + +You can also identify hashes:: + >>> linux_context.identify(hash) + 'sha512_crypt' + +Or encrypt using a specific algorithm:: + >>> linux_context.schemes() + ('sha512_crypt', 'sha256_crypt', 'md5_crypt', 'des_crypt', 'unix_disabled') + >>> linux_context.encrypt("password", scheme="des_crypt") + '2fmLLcoHXuQdI' + >>> linux_context.identify('2fmLLcoHXuQdI') + 'des_crypt' + +.. seealso:: + the :ref:`CryptContext Tutorial ` + and :ref:`CryptContext Reference ` + for more information about the CryptContext class. Unix Password Hashes ==================== -PassLib provides a number of pre-configured :class:`!CryptContext` instances +Passlib provides a number of pre-configured :class:`!CryptContext` instances which can identify and manipulate all the formats used by Linux and BSD. See the :ref:`modular crypt identifier list ` for a complete list of which hashes are supported by which operating system. Predefined Contexts ------------------- -PassLib provides :class:`!CryptContext` instances +Passlib provides :class:`!CryptContext` instances for the following Unix variants: .. data:: linux_context @@ -49,27 +92,10 @@ .. note:: - All of the above contexts include the :class:`~passlib.hash.unix_fallback` handler + All of the above contexts include the :class:`~passlib.hash.unix_disabled` handler as a final fallback. This special handler treats all strings as invalid passwords, particularly the common strings ``!`` and ``*`` which are used to indicate - that an account has been disabled [#shadow]_. It can also be configured - to treat empty strings as a wildcard allowing in all passwords, - though this behavior is disabled by default for security reasons. - -A quick usage example, using the :data:`!linux_context` instance:: - - >>> from passlib.hosts import linux_context - >>> hash = linux_context.encrypt("password") - >>> hash - '$6$rounds=31779$X2o.7iqamZ.bAigR$ojbo/zh6sCmUuibhM7lnqR4Vy0aB3xGZXOYVLgtTFgNYiXaTNn/QLUz12lDSTdxJCLXHzsHiWCsaryAlcbAal0' - >>> linux_context.verify("password", hash) - True - >>> linux_context.identify(hash) - 'sha512_crypt' - >>> linux_context.encrypt("password", scheme="des_crypt") - '2fmLLcoHXuQdI' - >>> linux_context.identify('2fmLLcoHXuQdI') - 'des_crypt' + that an account has been disabled [#shadow]_. Current Host OS --------------- @@ -83,7 +109,7 @@ The main differences between this object and :func:`!crypt`: * this object provides introspection about *which* schemes - are available on a given system (via ``host_context.policy.schemes()``). + are available on a given system (via ``host_context.schemes()``). * it defaults to the strongest algorithm available, automatically configured to an appropriate strength for encrypting new passwords. @@ -93,7 +119,7 @@ As an example, this can be used in conjunction with stdlib's :mod:`!spwd` module to verify user passwords on the local system:: - >>> #NOTE/WARNING: this example requires running as root on most systems. + >>> # NOTE/WARNING: this example requires running as root on most systems. >>> import spwd, os >>> from passlib.hosts import host_context >>> hash = spwd.getspnam(os.environ['USER']).sp_pwd diff -Nru passlib-1.5.3/docs/lib/passlib.registry.rst passlib-1.6.1/docs/lib/passlib.registry.rst --- passlib-1.5.3/docs/lib/passlib.registry.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.registry.rst 2012-08-01 17:25:10.000000000 +0000 @@ -5,12 +5,22 @@ .. module:: passlib.registry :synopsis: registry for tracking password hash handlers. -This module contains the code PassLib uses to track all password hash handlers +This module contains the code Passlib uses to track all password hash handlers that it knows about. While custom handlers can be used directly within an application, or even handed to a :class:`!CryptContext`; it is frequently useful to register them globally within a process and then refer to them by name. This module provides facilities for that, as well as programmatically -querying passlib to detect what algorithms are available. +querying Passlib to detect what algorithms are available. + +.. warning:: + + This module is primarily used as an internal support module. + It's interface has not been finalized yet, and may be changed somewhat + between major releases of Passlib, as the internal code is cleaned up + and simplified. + + Applications should access hashes through the :mod:`passlib.hash` module + where possible (new ones may also be registered by writing to that module). Interface ========= @@ -31,19 +41,19 @@ ===== Example showing how to use :func:`!registry_crypt_handler_path`:: - >>> #register the location of a handler without loading it + >>> # register the location of a handler without loading it >>> from passlib.registry import register_crypt_handler_path >>> register_crypt_handler_path("myhash", "myapp.support.hashes") - >>> #even before being loaded, it's name will show up as available + >>> # even before being loaded, it's name will show up as available >>> from passlib.registry import list_crypt_handlers >>> 'myhash' in list_crypt_handlers() True >>> 'myhash' in list_crypt_handlers(loaded_only=True) False - >>> #when the name "myhash" is next referenced, - >>> #the class "myhash" will be imported from the module "myapp.support.hashes" + >>> # when the name "myhash" is next referenced, + >>> # the class "myhash" will be imported from the module "myapp.support.hashes" >>> from passlib.context import CryptContext >>> cc = CryptContext(schemes=["myhash"]) #<-- this will cause autoimport diff -Nru passlib-1.5.3/docs/lib/passlib.utils.compat.rst passlib-1.6.1/docs/lib/passlib.utils.compat.rst --- passlib-1.5.3/docs/lib/passlib.utils.compat.rst 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.utils.compat.rst 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,61 @@ +====================================================== +:mod:`passlib.utils.compat` - Python 2/3 Compatibility +====================================================== + +.. module:: passlib.utils.compat + :synopsis: python 2/3 compatibility wrappers + +This module contains a number of wrapper functions used by Passlib +to run under Python 2 and 3 without changes. + +.. todo:: + + finish documenting this module. + +Unicode Helpers +=============== +.. autofunction:: uascii_to_str +.. autofunction:: str_to_uascii + +.. function:: join_unicode + + Join a sequence of unicode strings, e.g. + ``join_unicode([u"a",u"b",u"c"]) -> u"abc"``. + +Bytes Helpers +============= +.. autofunction:: bascii_to_str +.. autofunction:: str_to_bascii + +.. function:: join_bytes + + Join a sequence of byte strings, e.g. + ``join_bytes([b"a",b"b",b"c"]) -> b"abc"``. + +.. function:: join_byte_values + + Join a sequence of integers into a byte string, + e.g. ``join_byte_values([97,98,99]) -> b"abc"``. + +.. function:: join_byte_elems + + Join a sequence of byte elements into a byte string. + + Python 2 & 3 return different things when accessing + a single element of a byte string: + + * Python 2 returns a 1-element byte string (e.g. ``b"abc"[0] -> b"a"``). + * Python 3 returns the ordinal value (e.g. ``b"abc"[0] -> 97``). + + This function will join a sequence of the appropriate type + for the given python version -- under Python 2, this is an alias + for :func:`join_bytes`, under Python 3 this is an alias for :func:`join_byte_values`. + +.. function:: byte_elem_value + + Function to convert byte element to integer (a no-op under PY3) + +.. function:: iter_byte_values + + Function to iterate over a byte string as a series of integers. + (This is just the native bytes iterator under PY3). diff -Nru passlib-1.5.3/docs/lib/passlib.utils.des.rst passlib-1.6.1/docs/lib/passlib.utils.des.rst --- passlib-1.5.3/docs/lib/passlib.utils.des.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.utils.des.rst 2012-08-01 19:23:37.000000000 +0000 @@ -12,10 +12,10 @@ should not be used in new applications. This module contains routines for encrypting blocks of data using the DES algorithm. -They do not support multi-block operation or decryption, +Note that these functions do not support multi-block operation or decryption, since they are designed primarily for use in password hash algorithms (such as :class:`~passlib.hash.des_crypt` and :class:`~passlib.hash.bsdi_crypt`). .. autofunction:: expand_des_key .. autofunction:: des_encrypt_block -.. autofunction:: mdes_encrypt_int_block +.. autofunction:: des_encrypt_int_block diff -Nru passlib-1.5.3/docs/lib/passlib.utils.h64.rst passlib-1.6.1/docs/lib/passlib.utils.h64.rst --- passlib-1.5.3/docs/lib/passlib.utils.h64.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.utils.h64.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -================================================ -:mod:`passlib.utils.h64` - Hash-64 Codec helpers -================================================ - -.. module:: passlib.utils.h64 - :synopsis: Hash-64 Codec helpers - -Many of the password hash algorithms in passlib -use a encoding scheme very similar to (but not compatible with) -the standard base64 encoding scheme. the main differences are that -it assigns the characters *completely* different numeric values compared -to base64, as well as using ``.`` instead of ``+`` in it's character set. - -This encoding system appears to have originated with des-crypt hash, -but is used by md5-crypt, sha-256-crypt, and others. -within passlib, this encoding is referred as ``hash64`` encoding, -and this module contains various utilities functions for encoding -and decoding strings in that format. - -.. note:: - It may *look* like bcrypt uses this scheme, - when in fact bcrypt uses yet another ordering, - which does not match hash64 or other base64 schemes. - -.. _h64charset: - -Constants -========= -.. data:: CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - - The character set used by the Hash-64 format. - A character's index in CHARS denotes it's corresponding 6-bit integer value. - -Bytes <-> Hash64 -================ - -.. autofunction:: encode_bytes -.. autofunction:: decode_bytes - -.. autofunction:: encode_transposed_bytes -.. autofunction:: decode_transposed_bytes - -Int <-> Hash64 -============== - -.. autofunction:: decode_int6 -.. autofunction:: encode_int6 - -.. autofunction:: decode_int12 -.. autofunction:: encode_int12 - -.. autofunction:: decode_int24 -.. autofunction:: encode_int24 - -.. autofunction:: decode_int64 -.. autofunction:: encode_int64 diff -Nru passlib-1.5.3/docs/lib/passlib.utils.handlers.rst passlib-1.6.1/docs/lib/passlib.utils.handlers.rst --- passlib-1.5.3/docs/lib/passlib.utils.handlers.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.utils.handlers.rst 2012-08-01 17:05:50.000000000 +0000 @@ -2,11 +2,18 @@ pair: custom hash handler; implementing ========================================================================== -:mod:`passlib.utils.handlers` - Helpers for writing password hash handlers +:mod:`passlib.utils.handlers` - Framework for writing password hashes ========================================================================== .. module:: passlib.utils.handlers - :synopsis: helper classes for writing password hash handlers + :synopsis: framework for writing password hashes + +.. warning:: + + This module is primarily used as an internal support module. + It's interface has not been finalized yet, and may be changed somewhat + between major releases of Passlib, as the internal code is cleaned up + and simplified. .. todo:: @@ -17,37 +24,37 @@ Implementing Custom Handlers ============================ All that is required in order to write a custom handler that will work with -PassLib is to create an object (be it module, class, or object) that +Passlib is to create an object (be it module, class, or object) that exposes the functions and attributes required by the :ref:`password-hash-api`. -For classes, PassLib does not make any requirements about what a class instance +For classes, Passlib does not make any requirements about what a class instance should look like (if the implementation even uses them). -That said, most of the handlers built into PassLib are based around the :class:`GenericHandler` +That said, most of the handlers built into Passlib are based around the :class:`GenericHandler` class, and it's associated mixin classes. While deriving from this class is not required, doing so will greatly reduce the amount of addition code that is needed for all but the most convoluted password hash schemes. Once a handler has been written, it may be used explicitly, passed into an application's custom :class:`CryptContext` directly, or registered -globally with PassLib via the :mod:`passlib.registry` module. +globally with Passlib via the :mod:`passlib.registry` module. See :ref:`testing-hash-handlers` for details about how to test -custom handlers against PassLib's unittest suite. +custom handlers against Passlib's unittest suite. The GenericHandler Class ======================== Design ------ -Most of the handlers built into PassLib are based around the :class:`GenericHandler` +Most of the handlers built into Passlib are based around the :class:`GenericHandler` class. This class is designed under the assumption that the common workflow for hashes is some combination of the following: 1. parse hash into constituent parts - performed by :meth:`~GenericHandler.from_string`. 2. validate constituent parts - performed by :class:`!GenericHandler`'s constructor, - and the normalization functions such as :meth:`~GenericHandler.norm_checksum` and :meth:`~HasSalt.norm_salt` + and the normalization functions such as :meth:`~GenericHandler._norm_checksum` and :meth:`~HasSalt._norm_salt` which are provided by it's related mixin classes. -3. calculate the raw checksum for a specific password - performed by :meth:`~GenericHandler.calc_checksum`. +3. calculate the raw checksum for a specific password - performed by :meth:`~GenericHandler._calc_checksum`. 4. assemble hash, including new checksum, into a new string - performed by :meth:`~GenericHandler.to_string`. With this in mind, :class:`!GenericHandler` provides implementations @@ -80,7 +87,7 @@ (such as returned by :meth:`from_string`), returning a hash string. - * provide an implementation of the :meth:`calc_checksum` instance method. + * provide an implementation of the :meth:`_calc_checksum` instance method. this is the heart of the hash; this method should take in the password as the first argument, then generate and return the digest portion @@ -157,32 +164,32 @@ Usage ----- As an example of how to use :class:`!HandlerCase`, -the following is an annoted version +the following is an annotated version of the unittest for :class:`passlib.hash.des_crypt`:: from passlib.hash import des_crypt from passlib.tests.utils import HandlerCase - #create a subclass for the handler... + # create a subclass for the handler... class DesCryptTest(HandlerCase): "test des-crypt algorithm" - #: [required] - store the handler object itself in the handler attribute + # [required] - store the handler object itself in the handler attribute handler = des_crypt - #: [optional] - if your hash only uses the first X characters of the password, - #: set that value here. otherwise leave the default (-1). - secret_chars = 8 - - #: [required] - this should be a list of (password, hash) pairs, - # which should all verify correctly using your handler. - # it is recommend include pairs which test all of the following: + # [optional] - if your hash only uses the first X characters of the password, + # set that value here. otherwise leave the default (-1). + secret_size = 8 + + # [required] - this should be a list of (password, hash) pairs, + # which should all verify correctly using your handler. + # it is recommend include pairs which test all of the following: # - # * empty string & short strings for passwords - # * passwords with 2 byte unicode characters - # * hashes with varying salts, rounds, and other options + # * empty string & short strings for passwords + # * passwords with 2 byte unicode characters + # * hashes with varying salts, rounds, and other options known_correct_hashes = ( - #format: (password, hash) + # format: (password, hash) ('', 'OgAwTx2l6NADI'), (' ', '/Hk.VPuwQTXbc'), ('test', 'N1tQbOFcM5fpg'), @@ -192,13 +199,13 @@ (u'hell\u00D6', 'saykDgk3BPZ9E'), ) - #: [optional] - if there are hashes which are similar in format - #: to your handler, and you want to make sure :meth:`identify` - #: does not return ``True`` for such hashes, - #: list them here. otherwise this can be omitted. + # [optional] - if there are hashes which are similar in format + # to your handler, and you want to make sure :meth:`identify` + # does not return ``True`` for such hashes, + # list them here. otherwise this can be omitted. # known_unidentified_hashes = [ - #bad char in otherwise correctly formatted hash + # bad char in otherwise correctly formatted hash '!gAwTx2l6NADI', ] diff -Nru passlib-1.5.3/docs/lib/passlib.utils.pbkdf2.rst passlib-1.6.1/docs/lib/passlib.utils.pbkdf2.rst --- passlib-1.5.3/docs/lib/passlib.utils.pbkdf2.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.utils.pbkdf2.rst 2012-07-10 20:24:37.000000000 +0000 @@ -25,8 +25,9 @@ Helper Functions ================ +.. autofunction:: norm_hash_name .. autofunction:: get_prf .. given how this module is expanding in scope, - perhaps it should be renamed "kdf"? + perhaps it should be renamed "kdf" or "crypto"? diff -Nru passlib-1.5.3/docs/lib/passlib.utils.rst passlib-1.6.1/docs/lib/passlib.utils.rst --- passlib-1.5.3/docs/lib/passlib.utils.rst 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/docs/lib/passlib.utils.rst 2012-08-01 20:55:33.000000000 +0000 @@ -3,59 +3,154 @@ ============================================= .. module:: passlib.utils - :synopsis: helper functions for implementing password hashes + :synopsis: internal helpers for implementing password hashes -This module contains a number of utility functions used by passlib -to implement the builtin handlers, and other code within passlib. +This module contains a number of utility functions used by Passlib +to implement the builtin hashes and other internals. They may also be useful when implementing custom handlers for existing legacy formats. +.. warning:: + + This module is primarily used as an internal support module. + It's interface has not been finalized yet, and may be changed somewhat + between major releases of Passlib, as the internal code is cleaned up + and simplified. + Constants ========= -.. data:: sys_bits +.. + .. data:: sys_bits - Native bit size of host architecture (either 32 or 64 bit). - used for various purposes internally. + Native bit size of host architecture (either 32 or 64 bit). + used for various purposes internally. .. data:: unix_crypt_schemes - List of the names of all the handlers in :mod:`passlib.hash` - which are supported by the native :func:`crypt()` function - of at least one OS. + List of the names of all the hashes in :mod:`passlib.hash` + which are natively supported by :func:`crypt` on at least one operating + system. For all hashes in this list, the expression - ``get_crypt_handler(name).has_backend("os_crypt")`` - will return ``True`` iff there is native OS support for that hash. - + :samp:`passlib.hash.{alg}.has_backend("os_crypt")` + will return ``True`` if the host OS natively supports the hash. This list is used by :data:`~passlib.hosts.host_context` and :data:`~passlib.apps.ldap_context` to determine which hashes are supported by the host. - See :ref:`mcf-identifiers` for a table of which OSes - are known to support which hashes. - -.. autoexception:: MissingBackendError + .. seealso:: :ref:`mcf-identifiers` for a table of which OSes are known to support which hashes. -Decorators -========== -.. autofunction:: classproperty - -String Manipulation -=================== -.. autofunction:: splitcomma +.. + PYPY + JYTHON + rounds_cost_values + +.. + Decorators + ========== + .. autofunction:: classproperty + +Unicode Helpers +=============== +.. autofunction:: consteq +.. autofunction:: saslprep -Bytes Manipulation -================== - -.. autofunction:: bytes_to_int -.. autofunction:: int_to_bytes +Bytes Helpers +============= .. autofunction:: xor_bytes +.. autofunction:: render_bytes +.. autofunction:: int_to_bytes +.. autofunction:: bytes_to_int + +Encoding Helpers +================ +.. autofunction:: is_same_codec +.. autofunction:: is_ascii_codec +.. autofunction:: is_ascii_safe +.. autofunction:: to_bytes +.. autofunction:: to_unicode +.. autofunction:: to_native_str + +Base64 Encoding +=============== + +Base64Engine Class +------------------ +Passlib has to deal with a number of different Base64 encodings, +with varying endianness, as well as wildly different character <-> value +mappings. This is all encapsulated in the :class:`Base64Engine` class, +which provides common encoding actions for an arbitrary base64-style encoding +scheme. There are also a couple of predefined instances which are commonly +used by the hashes in Passlib. + +.. autoclass:: Base64Engine + +Common Character Maps +--------------------- +.. data:: BASE64_CHARS + + Character map used by standard MIME-compatible Base64 encoding scheme. + +.. data:: HASH64_CHARS + + Base64 character map used by a number of hash formats; + the ordering is wildly different from the standard base64 character map. + + This encoding system appears to have originated with + :class:`~passlib.hash.des_crypt`, but is used by + :class:`~passlib.hash.md5_crypt`, :class:`~passlib.hash.sha256_crypt`, + and others. Within Passlib, this encoding is referred as the "hash64" encoding, + to distinguish it from normal base64 and others. + +.. data:: BCRYPT_CHARS + + Base64 character map used by :class:`~passlib.hash.bcrypt`. + The ordering is wildly different from both the standard base64 character map, + and the common hash64 character map. + +Predefined Instances +-------------------- +.. data:: h64 + + Predefined instance of :class:`Base64Engine` which uses + the :data:`!HASH64_CHARS` character map and little-endian encoding. + (see :data:`HASH64_CHARS` for more details). + +.. data:: h64big + + Predefined variant of :data:`h64` which uses big-endian encoding. + This is mainly used by :class:`~passlib.hash.des_crypt`. + +.. versionchanged:: 1.6 + Previous versions of Passlib contained + a module named :mod:`!passlib.utils.h64`; As of Passlib 1.6 this + was replaced by the the ``h64`` and ``h64big`` instances of + the :class:`Base64Engine` class; + the interface remains mostly unchanged. + + +Other +----- +.. autofunction:: ab64_encode +.. autofunction:: ab64_decode + +.. + .. data:: AB64_CHARS + + Variant of standard Base64 character map used by some + custom Passlib hashes (see :func:`ab64_encode`). + +.. + Host OS + ======= + .. autofunction:: safe_crypt + .. autofunction:: tick Randomness ========== .. data:: rng - The random number generator used by passlib to generate + The random number generator used by Passlib to generate salt strings and other things which don't require a cryptographically strong source of randomness. @@ -66,11 +161,10 @@ .. autofunction:: getrandbytes .. autofunction:: getrandstr +.. autofunction:: generate_password(size=10, charset=) -.. autofunction:: generate_password(size=10, charset=) - -Object Tests -============ +Interface Tests +=============== .. autofunction:: is_crypt_handler .. autofunction:: is_crypt_context .. autofunction:: has_rounds_info @@ -83,8 +177,9 @@ .. toctree:: :maxdepth: 1 + passlib.utils.handlers passlib.utils.des - passlib.utils.h64 - passlib.utils.md4 passlib.utils.pbkdf2 - passlib.utils.handlers + +.. + passlib.utils.compat diff -Nru passlib-1.5.3/docs/make.py passlib-1.6.1/docs/make.py --- passlib-1.5.3/docs/make.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/make.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -"Makefile for Sphinx documentation, adapted to python" -import os -from cloud_sptheme.make_helper import SphinxMaker -if __name__ == "__main__": - SphinxMaker.execute(root_dir=os.path.join(__file__,os.pardir)) diff -Nru passlib-1.5.3/docs/modular_crypt_format.rst passlib-1.6.1/docs/modular_crypt_format.rst --- passlib-1.5.3/docs/modular_crypt_format.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/modular_crypt_format.rst 2012-08-01 20:18:32.000000000 +0000 @@ -40,54 +40,55 @@ ============ Unfortunately, there is no specification document for this format. Instead, it exists in *de facto* form only; the following -is an attempt to roughly identify the guidelines followed -by the modular crypt format hashes found in passlib: +is an attempt to roughly identify the conventions followed +by the modular crypt format hashes found in Passlib: -1. Hash strings must use only 7-bit ascii characters. +1. Hash strings should use only 7-bit ascii characters. No known OS or application generates hashes which violate this rule. - However, some systems (eg Linux's shadow routines) will happily - and correctly accept hashes which contain 8-bit characters in their salt. + However, some systems (e.g. Linux) will happily + accept hashes which contain 8-bit characters in their salt, This is probably a case of "permissive in what you accept, strict in what you generate". -2. Hash strings should always start with the prefix :samp:`${identifier}$`, +2. Hash strings should start with the prefix :samp:`${identifier}$`, where :samp:`{identifier}` is a short string uniquely identifying hashes generated by that algorithm, using only lower case ascii - letters, numbers, and hyphens. + letters, numbers, and hyphens + (c.f. the list of :ref:`known identifiers ` below). - Initially, most schemes adhereing to this format - only used a single digit to identify the hash - (eg ``$1$`` for :class:`!md5_crypt`). - Because of this, many systems only look at the first + When MCF was first introduced, most schemes choose a single digit + as their identifier (e.g. ``$1$`` for :class:`~passlib.hash.md5_crypt`). + Because of this, some older systems only look at the first character when attempting to distinguish hashes. + However, as Unix variants have branched off, + new schemes were developed which used larger + identifying strings (e.g. ``$sha1$`` for :class:`~passlib.hash.sha1_crypt`). - Despite this, as Unix systems have branched off, - new hashes have been developed which used larger - identifying strings (eg ``$sha1$`` for :class:`~passlib.hash.sha1_crypt`); - so in general identifier strings should not be assumed to use a single character. + At this point, any new hash schemes should probably use a 6-8 character + descriptive identifier, to avoid potential namespace clashes. -3. Hashes should contain only ascii letters ``a``-``z`` and ``A``-``Z``, +3. Hashes should only contain the ascii letters ``a``-``z`` and ``A``-``Z``, ascii numbers 0-9, and the characters ``./``; though additionally - they should use the ``$`` character as an internal field separator. + they may use the ``$`` character as an internal field separator. - This is the least adhered-to of any modular crypt format rule. + This is the least adhered-to of any modular crypt format convention. Other characters (such as ``=,-``) are sometimes used by various formats, though sparingly. The only hard and fast stricture - is that ``:;!*`` and non-printable characters be avoided, - since this would interfere with parsing of /etc/shadow + is that ``:;!*`` and all non-printable characters be avoided, + since this would interfere with parsing of the Unix shadow password file, where these hashes are typically stored. Pretty much all modular-crypt-format hashes use ascii letters, numbers, ``.``, and ``/`` to provide base64 encoding of their raw data, though the exact character value assignments vary between hashes - (see :mod:`passlib.utils.h64`). + (see :data:`passlib.utils.h64`). -4. Hash schemes should put their "checksum" portion - at the end of the hash, preferrably separated +4. Hash schemes should put their "digest" portion + at the end of the hash, preferably separated by a ``$``. This allows password hashes to be easily truncated @@ -99,12 +100,13 @@ in order to verify a password, without having to perform excessive parsing. - Most modular crypt format hashes follow this, - though some (like :class:`~passlib.hash.bcrypt`) omit the ``$`` separator. + Most modular crypt format hashes follow this convention, + though some (like :class:`~passlib.hash.bcrypt`) omit the ``$`` separator + between the configuration and the digest. - As well, there is no set standard about whether configuration + Furthermore, there is no set standard about whether configuration strings should or should not include a trailing ``$`` at the end, - though the general rule is that a hash behave the same regardless + though the general rule is that hashing should behave the same in either case (:class:`~passlib.hash.sun_md5_crypt` behaves particularly poorly regarding this last point). @@ -122,47 +124,66 @@ Identifiers & Platform Support ============================== +OS Defined Hashes +----------------- The following table lists of all the major MCF hashes supported by Passlib, -and indicates which operating systems [#gae]_ offer native support. - -.. todo:: include MacOS X in this list +and indicates which operating systems offer native support: ==================================== ==================== =========== =========== =========== =========== ======= Scheme Prefix Linux FreeBSD NetBSD OpenBSD Solaris ==================================== ==================== =========== =========== =========== =========== ======= -:class:`~passlib.hash.des_crypt` n/a y y y y y -:class:`~passlib.hash.bsdi_crypt` ``_`` y y +:class:`~passlib.hash.des_crypt` y y y y y +:class:`~passlib.hash.bsdi_crypt` ``_`` y y y :class:`~passlib.hash.md5_crypt` ``$1$`` y y y y y -:class:`~passlib.hash.sun_md5_crypt` ``$md5$``, ``$md5,`` y -:class:`~passlib.hash.bcrypt` ``$2$``, ``$2a$`` y y y y -:class:`~passlib.hash.nthash` ``$3$`` y -:class:`~passlib.hash.sha256_crypt` ``$5$`` y y -:class:`~passlib.hash.sha512_crypt` ``$6$`` y y +:class:`~passlib.hash.bcrypt` ``$2$``, ``$2a$``, + ``$2x$``, ``$2y$`` y y y y +:class:`~passlib.hash.bsd_nthash` ``$3$`` y +:class:`~passlib.hash.sha256_crypt` ``$5$`` y 8.3+ y +:class:`~passlib.hash.sha512_crypt` ``$6$`` y 8.3+ y +:class:`~passlib.hash.sun_md5_crypt` ``$md5$``, ``$md5,`` y :class:`~passlib.hash.sha1_crypt` ``$sha1$`` y ==================================== ==================== =========== =========== =========== =========== ======= -The following table lists the other MCF hashes supported by Passlib, -most of which are only used by applications: +Additional Platforms +-------------------- +The modular crypt format is also supported to some degree +by the following operating systems and platforms: + +.. rst-class:: html-plain-table + +===================== ============================================================== +**MacOS X** Darwin's native :func:`!crypt` provides limited functionality, + supporting only :class:`~passlib.hash.des_crypt` and + :class:`~passlib.hash.bsdi_crypt`. + +**Google App Engine** As of 2011-08-19, Google App Engine's :func:`!crypt` + implementation appears to match that of a typical Linux + system. +===================== ============================================================== + +Application-Defined Hashes +-------------------------- +The following table lists the other MCF hashes supported by Passlib. +These hashes can be found in various libraries and applications +(and are not natively supported by any known OS): =========================================== =================== =========================== Scheme Prefix Primary Use (if known) =========================================== =================== =========================== :class:`~passlib.hash.apr_md5_crypt` ``$apr1$`` Apache htdigest files :class:`~passlib.hash.phpass` ``$P$``, ``$H$`` PHPass-based applications -:class:`~passlib.hash.pbkdf2_sha1` ``$pbkdf2$`` -:class:`~passlib.hash.pbkdf2_sha256` ``$pbkdf2-sha256$`` -:class:`~passlib.hash.pbkdf2_sha512` ``$pbkdf2-sha512$`` +:class:`~passlib.hash.pbkdf2_sha1` ``$pbkdf2$`` Passlib-specific +:class:`~passlib.hash.pbkdf2_sha256` ``$pbkdf2-sha256$`` Passlib-specific +:class:`~passlib.hash.pbkdf2_sha512` ``$pbkdf2-sha512$`` Passlib-specific +:class:`~passlib.hash.scram` ``$scram$`` Passlib-specific :class:`~passlib.hash.cta_pbkdf2_sha1` ``$p5k2$`` [#cta]_ :class:`~passlib.hash.dlitz_pbkdf2_sha1` ``$p5k2$`` [#cta]_ =========================================== =================== =========================== .. rubric:: Footnotes -.. [#gae] As of 2011-08-19, Google App Engine's :mod:`crypt` implementation - appears to provide hash support matching that of a typical Linux system. - .. [#cta] :class:`!cta_pbkdf2_sha1` and :class:`!dlitz_pbkdf2_sha1` both use - the same identifier. They can be distinguished - by the fact that cta hashes will always end in ``=``, while dlitz + the same identifier. While there are other internal differences, + the two can be quickly distinguished + by the fact that cta hashes always end in ``=``, while dlitz hashes contain no ``=`` at all. - diff -Nru passlib-1.5.3/docs/new_app_quickstart.rst passlib-1.6.1/docs/new_app_quickstart.rst --- passlib-1.5.3/docs/new_app_quickstart.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/new_app_quickstart.rst 2012-08-02 15:01:12.000000000 +0000 @@ -16,31 +16,33 @@ For applications which want to quickly add password hashing, all they need to do is the following:: - >>> #import the context under an app-specific name (so it can easily be replaced later) + >>> # import the context under an app-specific name (so it can easily be replaced later) >>> from passlib.apps import custom_app_context as pwd_context - >>> #encrypting a password... + >>> # encrypting a password... >>> hash = pwd_context.encrypt("somepass") - >>> #verifying a password... + >>> # verifying a password... >>> ok = pwd_context.verify("somepass", hash) - >>> #[optional] encrypting a password for an admin account... - >>> # the custom_app_context is preconfigured so that - >>> # if the category is set to "admin" instead of None, - >>> # it uses a stronger setting of 80000 rounds: + >>> # [optional] encrypting a password for an admin account... + >>> # the custom_app_context is preconfigured so that + >>> # if the category is set to "admin" instead of None, + >>> # it uses a stronger setting of 80000 rounds: >>> hash = pwd_context.encrypt("somepass", category="admin") For applications which started using this preset, but whose needs have grown beyond it, it is recommended to create your own :mod:`CryptContext ` instance; see below for more... +.. index:: Passlib; recommended hash algorithms + .. _recommended-hashes: Choosing a Hash ================ *If you already know what hash algorithm(s) you want to use, -skip to the next section,* `Creating a CryptContext`_. +skip to the next section,* `Creating and Using a CryptContext`_. If you'd like to set up a configuration that's right for your application, the first thing to do is choose a password hashing scheme. @@ -66,7 +68,7 @@ The following comparison should help you choose which hash is most appropriate for your application; if in doubt, any of these is a good choice, though PBKDF2 is probably the best -for portability. +for portability. .. rst-class:: html-toggle @@ -86,23 +88,22 @@ Issues: Neither the original Blowfish, nor the modified version which BCrypt uses, have been NIST approved; this matter of concern is what motivated the development of SHA512-Crypt. -As well, it's rounds parameter is logarithmically scaled, +As well, its rounds parameter is logarithmically scaled, making it hard to fine-tune the amount of time taken to verify passwords; which can be an issue for applications that handle a large number -of simultaneous logon attempts (eg web apps). +of simultaneous logon attempts (e.g. web apps). .. note:: For BCrypt support on non-BSD systems, - Passlib requires a C-extension module - provided by the external - :ref:`PyBcrypt or BCryptor ` packages. - Neither of these currently supports Python 3. + Passlib requires the C-extension provided by + `py-bcrypt `_. + (py-bcrypt does not currently support Python 3). SHA512-Crypt ............ :class:`~passlib.hash.sha512_crypt` is -based on well-tested :class:`~passlib.hash.md5_crypt` +based on the well-tested :class:`~passlib.hash.md5_crypt` algorithm. In use since 2008, it's the default hash on most Linux systems; its direct ancestor :class:`!md5_crypt` has been in use since 1994 on most Unix systems. If you want your application's hashes to be readable by the @@ -112,24 +113,26 @@ :class:`~passlib.hash.ldap_sha512_crypt` and :class:`~passlib.hash.ldap_sha256_crypt`). -Issues: Like :class:`~passlib.hash.md5_crypt`, it's algorithm +Issues: Like :class:`~passlib.hash.md5_crypt`, its algorithm composes the underlying message digest hash in a baroque and somewhat arbitrary set combinations. -So far this "kitchen sink" design has been successful in it's +So far this "kitchen sink" design has been successful in its primary purpose: to prevent any attempts to create an optimized version for use in a pre-computed or brute-force search. However, this design also hampers analysis of the algorithm for future flaws. -This algorithm is probably the best choice for Google App Engine, +.. index:: Google App Engine; recommended hash algorithm + +:class:`~passlib.hash.sha512_crypt` is probably the best choice for Google App Engine, as Google's production servers appear to provide native support -via :mod:`crypt`, which will be used by Passlib. +via :mod:`crypt`, which will be used by Passlib. .. note:: - References to this algorithm are frequently confused with a raw SHA-512 hash; - while it uses SHA-512 as a cryptographic primitive, - this algorithm's resulting password hash is far more secure. + References to this algorithm are frequently confused with a raw SHA-512 hash. + While :class:`!sha512_crypt` uses the SHA-512 hash as a cryptographic primitive, + the algorithm's resulting password hash is far more secure. PBKDF2 ...... @@ -148,10 +151,10 @@ :class:`~passlib.hash.ldap_pbkdf2_sha256`). Issues: PBKDF2 has no security or portability issues. -However, it's only come into wide use as a password hash +However, it has only come into wide use as a password hash in recent years; mainly hampered by the fact that there is no standard format for encoding password hashes using this algorithm -(which is why Passlib has it's own :ref:`custom format `). +(which is why Passlib has its own :ref:`custom format `). .. note:: @@ -159,52 +162,66 @@ the external M2Crypto package to speed up PBKDF2 calculations, though this is not required. -Creating a CryptContext -======================= +.. index:: SCrypt; status of + +What about SCrypt? +.................. +`SCrypt `_ is the leading contender +to be the next-generation password hash algorithm. It offers many advances +over all of the above hashes; the primary feature being that it has +a variable *memory* cost as well as time cost. It is incredibly well designed, +and looks to likely replace all the others in this section. + +However, it is still young by comparison to the others; and has not been as throughly +tested, or widely implemented. The only Python wrapper that exists +does not even expose the underlying :func:`!scrypt` function, +but is rather a file encryption tool. +Due to these reasons, SCrypt has not yet been integrated into Passlib. + +.. seealso:: :issue:`8` of the Passlib bugtracker, for the current status of Passlib's SCrypt support. + +Creating and Using a CryptContext +================================= One you've chosen what password hash(es) you want to use, the next step is to define a :class:`~passlib.context.CryptContext` object to manage your hashes, and relating configuration information. Insert the following code into your application:: # - #import the CryptContext class, used to handle all hashing... + # import the CryptContext class, used to handle all hashing... # from passlib.context import CryptContext # - #create a single global instance for your app... + # create a single global instance for your app... # pwd_context = CryptContext( - #replace this list with the hash(es) you wish to support. - #this example sets pbkdf2_sha256 as the default, - #with support for legacy des_crypt hashes. + # replace this list with the hash(es) you wish to support. + # this example sets pbkdf2_sha256 as the default, + # with support for legacy des_crypt hashes. schemes=["pbkdf2_sha256", "des_crypt" ], default="pbkdf2_sha256", - #vary rounds parameter randomly when creating new hashes... - all__vary_rounds = "10%", + # vary rounds parameter randomly when creating new hashes... + all__vary_rounds = 0.1, - #set the number of rounds that should be used... - #(appropriate values may vary for different schemes, + # set the number of rounds that should be used... + # (appropriate values may vary for different schemes, # and the amount of time you wish it to take) pbkdf2_sha256__default_rounds = 8000, ) +To start using your CryptContext, import the context you created wherever it's needed:: -Using a CryptContext -==================== -To start using your CryptContext, import the context you created -in the previous section wherever needed:: - - >>> #import context from where you defined it... + >>> # import context from where you defined it... >>> from myapp.model.security import pwd_context - >>> #encrypting a password... + >>> # encrypting a password... >>> hash = pwd_context.encrypt("somepass") >>> hash '$pbkdf2-sha256$7252$qKFNyMYTmgQDCFDS.jRJDQ$sms3/EWbs4/3k3aOoid5azwq3HPZKVpUUrAsCfjrN6M' - >>> #verifying a password... + >>> # verifying a password... >>> pwd_context.verify("somepass", hash) True >>> pwd_context.verify("wrongpass", hash) @@ -212,13 +229,14 @@ .. seealso:: - * :mod:`passlib.hash` - list of all hashes supported by passlib. - * :mod:`passlib.context` - for more details about the CryptContext class. + * :mod:`passlib.hash` -- list of all hashes supported by passlib. + * :ref:`CryptContext Overview & Tutorial ` -- walkthrough of how to use the CryptContext class. + * :ref:`CryptContext Reference ` -- reference for the CryptContext api. .. rubric:: Footnotes .. [#choices] BCrypt, SHA-512 Crypt, and PBKDF2 are the most commonly - used password hashes as of May 2011, when this document - was written. You should make sure you are reading a current - copy of the passlib documentation, in case the state + used password hashes as of Aug 2012, when this document + last updated. You should make sure you are reading a current + copy of the Passlib documentation, in case the state of things has changed. diff -Nru passlib-1.5.3/docs/notes.txt passlib-1.6.1/docs/notes.txt --- passlib-1.5.3/docs/notes.txt 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/notes.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -==== -Todo -==== - -Internal Changes ----------------- -* C extensions to speed up some implementations -* py3k support - -Other Hash Formats ------------------- -* Mac OSX hash formats - -* SCrypt - http://www.tarsnap.com/scrypt.html - https://bitbucket.org/mhallin/py-scrypt/src - -Notes on various hash formats -============================= - -Cisco PIX ---------- -sample hashes found - http://www.freerainbowtables.com/phpBB3/viewtopic.php?f=2&t=1441 - - 8Ry2YjIyt7RRXU24 '' - 2KFQnbNIdI.2KYOU 'cisco' - hN7LzeyYjw12FSIU 'john'/'cisco' - 7DrfeZ7cyOj/PslD 'jack'/'cisco' - -alg - secret+user - truncate/pad-right-null to 16 bytes - md5().digest() - h64 encode - -todo: get some samples w/ passwords longer than 16 chars to verify - -Mac OSX -------- -Summary of info from http://www.dribin.org/dave/blog/archives/2006/04/28/os_x_passwords_2/ - -osx < 10.2 used /etc/passwd w/ DES-CRYPT - -osx 10.3 hash file (passwd "macintosh") - -D47F3AF827A48F7DFA4F2C1F12D68CD6 <-- nthash -08460EB13C5CA0C4CA9516712F7FED95 <-- lmhash -01424f955c11f92efef0b79d7fa3fb6be56a9f99 <-- sha1 - -osx 10.4 hash file (passwd "macintosh") -00000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -000000000E6A48F765D0FFFFF6247FA80D748E615F91DD0C7431E4D9000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -00000000000000000000000000000000000000000000000000000000000000000000000000000000\ -0000000000000000000000000000000000000000 - -offset 0-64 - nt hash + lm hash OR all zeros -offset 64 - 40 chars - raw sha1 password OR all zeroes (if from upgraded from 10.3) -offset 169-216 ( 48 chars) - salted sha1 hash - unhex first 8 chars + password | sha1 -> hexdigest diff -Nru passlib-1.5.3/docs/overview.rst passlib-1.6.1/docs/overview.rst --- passlib-1.5.3/docs/overview.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/overview.rst 2012-05-03 16:36:58.000000000 +0000 @@ -8,96 +8,74 @@ Passlib's contents can be roughly grouped into three categories: password hashes, password contexts, and utility functions. -.. note:: - - New applications which just need drop-in password hashing support - should see the :doc:`new_app_quickstart`. - Password Hashes =============== All of the hash schemes supported by Passlib are implemented -as classes importable from the :mod:`passlib.hash` module. -All of these classes support a single uniform interface of standard class methods. -These methods are documented in detail by the :ref:`password hash api `. - -As a quick example of how a password hash can be used directly:: - - >>> #import the SHA512-Crypt class: - >>> from passlib.hash import sha512_crypt as sc - - >>> #generate new salt, encrypt password: - >>> h = sc.encrypt("password") - >>> h - '$6$rounds=40000$xCsOXRqPPk5AGDFu$o5eyqxEoOSq0dLRFbPxEHp5Jc1vFVj47BNT.h9gmjSHXDS15mjIM.GSUaT5r6Z.Xa1Akrv4FAgKJE3EfbkJxs1' - - >>> #same, but with explict number of rounds: - >>> sc.encrypt("password", rounds=10000) - '$6$rounds=10000$QWT8AlDMYRms7vSx$.1267Pg6Opn9CblFndtBJ2Q0AI0fcI2IX93zX3gi1Qse./j.VlKYX59NIUlbs0A66wCbfu/vra9wMv2uwTZAI.' - - >>> #check if string is recognized as belonging to this hash scheme: - >>> sc.identify(h) - True - >>> #check if some other hash is recognized: - >>> sc.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') - False - - >>> #verify correct password: - >>> sc.verify("password", h) - True - >>> #verify incorrect password: - >>> sc.verify("secret", h) - False +as classes which can be imported from the :mod:`passlib.hash` module. +In turn, all of the hash classes implement a single uniform interface, +which is documented in detail and with usage examples in the +:ref:`password-hash-api` document. +However, many of these hashes are severely insecure, provided only for legacy +purposes, or are specialized in ways that are not generally useful. +If you are creating a new application and need to choose a password hash, +see the :doc:`new_app_quickstart`. + +.. seealso:: + + - :mod:`passlib.hash` -- all the hashes supported by Passlib. + - :ref:`password-hash-api` -- documentation of the common PasswordHash interface. + - :doc:`new_app_quickstart` -- choosing a hash for new applications. Password Contexts ================= -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. - -The :mod:`passlib.context` module provides the :class:`!CryptContext` class and other -utilties to help with these use-cases. This class handles -managing multiple password hash schemes, deprecation & migration of old hashes, and -many other policy requirements. - -A quick example of how a password context can be used:: - - >>> #importing the 'linux_context', which understands - >>> #all hashes found on standard linux systems: - >>> from passlib.hosts import linux_context as lc - - >>> #try encrypting a password - >>> lc.encrypt("password") - '$6$rounds=30000$suoPoYtkbccdZa3v$DW2KUcV98H4IrvlBB0YZf4DM8zqz5vduygB3OROhPzwHE5PDNVkpSUjJfjswn/dXqidha5t5CSCCIhtm6mIDR1' - - >>> #try encrypting a password using a specified scheme - >>> lc.encrypt("password", scheme="des_crypt") - 'q1Oyx5r9mdGZ2' - - >>> #try verifying a password (scheme is autodetected) - >>> lc.verify('password', 'q1Oyx5r9mdGZ2') - True - -Predefined Password Contexts -============================ -In addition to the :mod:`!passlib.context` module, -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 support a number of tasks: + +* add support for new algorithms, and deprecate old ones, +* raise the time-cost settings for existing algorithms, as computing power increases, +* and do rolling upgrades of existing hashes to comply with these changes. +* hardcode these policies in the source, or spend time implementing + a configuration language for them. + +In these situations, loading and handling multiple hash algorithms becomes +complicated and tedious. The :mod:`passlib.context` module provides a single class, +:class:`!CryptContext`, which attempts to solve all of these problems, +or at least relieve applications developers of (most of) the burden. +This class handles managing multiple password hash schemes, +deprecation & migration of old hashes, and supports a simple configuration +language that can be serialized to an INI file. + +.. seealso:: + + * :ref:`CryptContext Tutorial ` -- complete walkthrough of the CryptContext class. + * :ref:`CryptContext API Reference ` -- full method and attribute documentation. + +Application Helpers +=================== +Passlib also provides a number of pre-configured :class:`!CryptContext` instances in order to get users started quickly: -* The :mod:`passlib.apache` module contains classes - for managing htpasswd and htdigest files. + * :mod:`passlib.apps` -- contains pre-configured + instances for managing hashes used by Postgres, Mysql, and LDAP, and others. + + * :mod:`passlib.hosts` -- contains pre-configured + instances for managing hashes as found in the /etc/shadow files + on Linux and BSD systems. + +Passlib also contains a couple of additional modules which provide +support for certain application-specific tasks: -* The :mod:`passlib.apps` module contains pre-configured - instances for managing hashes used by Postgres, Mysql, and LDAP, and others. + * :mod:`passlib.apache` -- classes for managing htpasswd and htdigest files. -* The :mod:`passlib.hosts` module contains pre-configured - instances for managing hashes as found in the /etc/shadow files - on Linux and BSD systems. + * :mod:`passlib.ext.django` -- Django plugin which monkeypatches support for (almost) any hash in Passlib. Utility Functions ================= -The :mod:`passlib.registry` and :mod:`passlib.utils` modules contain 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 these. +Additionally, Passlib contains a number of modules which are used internally +to implement the all of the other features. These may change between major +releases, and won't be needed by most users of Passlib. They are documented +mainly to aid in examining the source. + + * :mod:`passlib.exc` -- all the custom errors & warnings used by Passlib. + * :mod:`passlib.registry` -- functions for registering password hash algorithms. + * :mod:`passlib.utils` -- support functions for implementing password hashes. diff -Nru passlib-1.5.3/docs/password_hash_api.rst passlib-1.6.1/docs/password_hash_api.rst --- passlib-1.5.3/docs/password_hash_api.rst 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/docs/password_hash_api.rst 2012-08-02 17:28:38.000000000 +0000 @@ -1,608 +1,739 @@ .. index:: - single: password hash api + single: PasswordHash interface single: custom hash handler; requirements -.. currentmodule:: passlib.hash +.. module:: passlib.ifc + :synopsis: abstract interfaces used by Passlib .. _password-hash-api: -================= -Password Hash API -================= +============================================= +Password Hash Interface +============================================= Overview ======== -All of the hashes supported by PassLib are implemented using classes [#otypes]_ -which support an identical interface; this document describes that -interface in terms of a non-existent abstract class called :class:`!PasswordHash`. -All of the supported password hashes [#supported]_ provide the following methods and attributes: +While the exact options and behavior will vary between algorithms, +all of the hashes provided by Passlib use the same interface, +defined by the following abstract base class: -:ref:`required-attributes` +.. class:: PasswordHash() - These consist of the attributes :attr:`~PasswordHash.name`, - :attr:`~PasswordHash.setting_kwds`, and :attr:`~PasswordHash.context_kwds`. - They permit users and applications to detect what features a specific :class:`!PasswordHash` - allows and/or requires. + This class provides an abstract interface for + an arbitrary password hashing algorithm. + While it offers a number of methods and attributes, + but most applications will only need the two primary methods: -:ref:`application-methods` + * :meth:`~PasswordHash.encrypt` - generate new salt, return hash of password. + * :meth:`~PasswordHash.verify` - verify password against existing hash. - This interface consists of the :meth:`~PasswordHash.encrypt`, - :meth:`~PasswordHash.identify`, and :meth:`~PasswordHash.verify` classmethods. - These are the methods most applications will need to make use of. + While not needed by most applications, the following methods + provide an interface that mimics the traditional Unix :func:`crypt` + function: -:ref:`crypt-methods` + * :meth:`~PasswordHash.genconfig` - create configuration string from salt & other options. + * :meth:`~PasswordHash.genhash` - hash password using existing hash or configuration string. - This interface consists of the :meth:`~PasswordHash.genconfig` - and :meth:`~PasswordHash.genhash` classmethods. - These methods mimic the standard unix crypt interface, - and are not usually needed by applications. + One additional support method is provided: -:ref:`optional-attributes` + * :meth:`~PasswordHash.identify` - check if hash belongs to this algorithm. - These attributes provide additional information - about the capabilities and limitations of certain password hash schemes. + Each hash algorithm also provides a number of :ref:`informational attributes `, + allowing programmatic inspection of it's options and parameter limits. -Usage -===== -While most uses of PassLib are done through a :class:`~passlib.context.CryptContext` class, -the various :class:`!PasswordHash` classes can be used directly to manipulate -passwords:: +.. _password-hash-examples: - >>> # for example, the SHA256-Crypt class: - >>> from passlib.hash import sha256_crypt as sc +Usage Examples +============== +The following code shows how to use the primary +methods of the :class:`~passlib.ifc.PasswordHash` interface -- +:meth:`~PasswordHash.encrypt` and :meth:`~PasswordHash.verify` -- +using the :class:`~passlib.hash.sha256_crypt` hash as an example:: - >>> # using it to encrypt a password: - >>> h = sc.encrypt("password") - >>> h + >>> # import the handler class + >>> from passlib.hash import sha256_crypt + + >>> # hash a password using the default settings: + >>> hash = sha256_crypt.encrypt("password") + >>> hash '$5$rounds=40000$HIo6SCnVL9zqF8TK$y2sUnu13gp4cv0YgLQMW56PfQjWaTyiHjVbXTgleYG9' - >>> # subsequent calls to sc.encrypt() will generate a new salt: - >>> sc.encrypt("password") + >>> # note that each call to encrypt() generates a new salt, + >>> # and thus the contents of the hash will differ, despite using the same password: + >>> sha256_crypt.encrypt("password") '$5$rounds=40000$1JfxoiYM5Pxokyh8$ez8uV8jjXW7SjpaTg2vHJmx3Qn36uyZpjhyC9AfBi7B' - >>> # the same, but with an explict number of rounds: - >>> sc.encrypt("password", rounds=10000) - '$5$rounds=10000$UkvoKJb8BPrLnR.D$OrUnOdr.IJx74hmyyzuRdr5k9lSXdkFxKmr7bLQTty5' - - >>> #the identify method can be used to determine the format of an unknown hash: - >>> sc.identify(h) + >>> # if the hash supports a variable number of iterations (which sha256_crypt does), + >>> # you can override the default value via the 'rounds' keyword: + >>> sha256_crypt.encrypt("password", rounds=12345) + '$5$rounds=12345$UeVpHaN2YFDwBoeJ$NJN8DwVZ4UfQw6.ijJZNWoZtk1Ivi5YfKCDsI2HzSq2' + ^^^^^ + + >>> # on the other end of things, the verify() method takes care of + >>> # checking if a password matches an existing hash string: + >>> sha256_crypt.verify("password", hash) True + >>> sha256_crypt.verify("letmeinplz", hash) + False - >>> #check if some other hash is recognized (in this case, an MD5-Crypt hash) - >>> sc.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31') +.. note:: + + Whether a hash supports a particular configuration keywoard (such as ``rounds``) + can be determined from it's documentation page; but also programmatically from + it's :attr:`~PasswordHash.setting_kwds` attribute. + +That concludes the most basic example, but there are a few more +common use-cases, such as how to use the :meth:`~PasswordHash.identify` method:: + + >>> # attempting to call verify() with another algorithm's hash will result in a ValueError: + >>> from passlib.hash import sha256_crypt, md5_crypt + >>> other_hash = md5_crypt.encrypt("password") + >>> sha256_crypt.verify("password", other_hash) + Traceback (most recent call last): + + ValueError: not a valid sha256_crypt hash + + >>> # this can be prevented by using the identify method, + >>> # determines whether a hash belongs to a given algorithm: + >>> hash = sha256_crypt.encrypt("password") + >>> sha256_crypt.identify(hash) + True + >>> sha256_crypt.identify(other_hash) False - >>> #the verify method encapsulates all hash comparison logic for a class: - >>> sc.verify("password", h) +While the initial :meth:`~PasswordHash.encrypt` example works for most hashes, +a small number of algorithms require you provide external data +(such as a username) every time a hash is calculated. +An example of this is the :class:`~passlib.hash.oracle10` algorithm:: + + >>> # for oracle10, encrypt requires a username: + >>> from passlib.hash import oracle10 + >>> hash = oracle10.encrypt("secret", user="admin") + 'B858CE295C95193F' + + >>> # the difference between this and something like the rounds setting (above) + >>> # is that oracle10 also requires the username when verifying a hash: + >>> oracle10.verify("secret", hash, user="admin") True - >>> sc.verify("wrongpassword", h) + + >>> # if either the username OR password is wrong, verify() will fail: + >>> oracle10.verify("secret", hash, user="wronguser") + False + >>> oracle10.verify("wrongpassword", hash, user="admin") False -.. _required-attributes: + >>> # forgetting to include the username when it's required will cause a TypeError: + >>> hash = oracle10.encrypt("password") + Traceback (most recent call last): + + TypeError: user must be unicode or bytes, not None + +.. note:: + + Whether a hash requires external parameters (such as ``user``) + can be determined from it's documentation page; but also programmatically from + it's :attr:`~PasswordHash.context_kwds` attribute. + +.. _primary-methods: + +Primary Methods +=============== +Most applications will only need to use two methods: +:meth:`~PasswordHash.encrypt` to generate new hashes, and :meth:`~PasswordHash.verify` +to check passwords against existing hashes. +These methods provide an easy interface for working with a password hash, +and abstract away details such as salt generation, hash normalization, +and hash comparison. + +.. classmethod:: PasswordHash.encrypt(secret, \*\*kwds) + + Digest password using format-specific algorithm, + returning resulting hash string. + + For most hashes supported by Passlib, this string will include + an algorithm identifier, a copy of the salt (if applicable), + and any other configuration information required to verify the password later. + + :type secret: unicode or bytes + :arg secret: string containing the password to encode. + + :param \*\*kwds: + + All additional keywords are algorithm-specific, and will be listed + in that hash's documentation; though many of the more common keywords + are listed under :attr:`~PasswordHash.setting_kwds` + and :attr:`~PasswordHash.context_kwds`. + Examples of common keywords include ``rounds`` and ``salt_size``. -Required Attributes -================================= -.. attribute:: PasswordHash.name + :returns: + Resulting hash of password, using an algorithm-specific format. - A unique name used to identify - the particular scheme this class implements. + This will always be an instance of :class:`!str` + (i.e. :class:`unicode` under Python 3, ``ascii``-encoded :class:`bytes` under Python 2). - These names should consist only of lowercase a-z, the digits 0-9, and underscores. + :raises ValueError: - .. note:: + * If a keyword's value is invalid (e.g. if a ``salt`` string + is too small, or a ``rounds`` value is out of range). - All handlers built into passlib are implemented as classes - located under :samp:`passlib.hash.{name}`, where :samp:`{name}` - is both the class name, and the value of the ``name`` attribute. - This is not a requirement, and may not be true for - externally-defined handlers. + * If the ``secret`` contains characters forbidden by the handler + (e.g. :class:`!des_crypt` forbids NULL characters). -.. attribute:: PasswordHash.setting_kwds + :raises TypeError: - If the scheme supports per-hash configuration - (such as salts, variable rounds, etc), this attribute - should contain a tuple of keywords corresponding - to each of those configuration options. - This should list all the main configuration keywords accepted - by :meth:`~PasswordHash.genconfig` and :meth:`~PasswordHash.encrypt`. - If no configuration options are supported, this attribute should be an empty tuple. - - While each class may support a variety of options, each with their own meaning - and semantics, the following keywords should have the same behavior - across all schemes which use them: + * if ``secret`` is not unicode or bytes. + * if a keyword argument had an incorrect type. + * if a required keyword was not provided. + + *(Note that the name of this method is a misnomer, password hashes + are typically based on irreversible cryptographic operations, + see* :issue:`21` *).* + + .. versionchanged:: 1.6 + Hashes now raise :exc:`TypeError` if a required keyword is missing, + rather than :exc:`ValueError` like in previous releases; in order + to conform with normal Python behavior. + + .. versionchanged:: 1.6 + Passlib is now much stricter about input validation: for example, + out-of-range ``rounds`` values now cause an error instead of being + clipped (though applications may set :ref:`relaxed=True ` + to restore the old behavior). - ``salt`` - If present, this means the algorithm contains some number of bits of salt - which should vary with every new hash created. +.. classmethod:: PasswordHash.verify(secret, hash, \*\*context_kwds) - Additionally, this means - :meth:`~PasswordHash.genconfig` and :meth:`~PasswordHash.encrypt` - should both accept an optional ``salt`` keyword allowing the user - to specify a bare salt string. Note that this feature is rarely - needed, and the constraints on the size & content of this string - will vary for each algorithm. + Verify a secret using an existing hash. - ``salt_size`` - Most algorithms which support ``salt`` will auto-generate a salt string - if none is provided. If this keyword is also present, it means it - can be used to select the size of the auto-generated salt. - If omitted, most algorithms will fall back to a default salt size. + This checks if a secret matches against the one stored + inside the specified hash. - ``rounds`` - If present, this means the algorithm allows for a variable number of rounds - to be used, allowing the processor time required to be increased. + :type secret: unicode or bytes + :param secret: + A string containing the password to check. - Providing this as a keyword should allow the application to - override the class' default number of rounds. While this - must be a non-negative integer for all implementations, - additional constraints may be present for each algorith - (such as the cost varying on a linear or logarithmic scale). + :type secret: unicode or bytes + :param hash: + A string containing the hash to check against, + such as returned by :meth:`~encrypt`. - ``ident`` - If present, the class supports multiple formats for encoding - the same hash. The class's documentation will generally list - the allowed values, allowing alternate output formats to be selected. + Hashes may be specified as :class:`!unicode` or + ``ascii``-encoded :class:`!bytes`. -.. attribute:: PasswordHash.context_kwds + :param \*\*kwds: + Very few hashes will have additional keywords. - This attribute should contain a tuple of keywords - which should be passed into :func:`encrypt`, :func:`verify`, - and :func:`genhash` in order to encrypt a password. - - Some algorithms require external contextual information - in order to generate a checksum for a password. - An example of this is :doc:`Postgres' MD5 algorithm `, - which requires the username be provided when generating a hash - (see that class for an example of how this works in pratice). - - Since most password hashes require no external information, - this tuple will usually be empty, and references - to context keywords can be ignored for all but a few classes. - - While each class may support a variety of options, each with their own meaning - and semantics, the following keywords should have the same behavior - across all schemes which use them: + The ones that do typically require external contextual information + in order to calculate the digest. For these hashes, + the values must match the ones passed to the original + :meth:`~PasswordHash.encrypt` call when the hash was generated, + or the password will not verify. + + These additional keywords are algorithm-specific, and will be listed + in that hash's documentation; though the more common keywords + are listed under :attr:`~PasswordHash.context_kwds`. + Examples of common keywords include ``user``. - ``user`` + :returns: + ``True`` if the secret matches, otherwise ``False``. - If present, the class requires a username be specified whenever - performing a hash calculation (eg: postgres_md5 and oracle10). + :raises TypeError: + * if either *secret* or *hash* is not a unicode or bytes instance. + * if the hash requires additional keywords which are not provided, + or have the wrong type. + + :raises ValueError: + * if *hash* does not match this algorithm's format. + * if the secret contains forbidden characters (see + :meth:`~PasswordHash.encrypt`). + * if a configuration/salt string generated by :meth:`~PasswordHash.genconfig` + is passed in as the value for *hash* (these strings look + similar to a full hash, but typically lack the digest portion + needed to verify a password). + + .. versionchanged:: 1.6 + This function now raises :exc:`ValueError` if a ``None`` or config string is provided + instead of a proper hash; previous releases were inconsistent + in their handling of these cases. -.. _application-methods: +.. _hash-unicode-behavior: + +.. note:: + + Regarding unicode passwords & non-ASCII characters: + + For the majority of hash algorithms and use-cases, passwords should + be provided as either :class:`!unicode` or ``utf-8``-encoded :class:`!bytes`. + There are only two major exceptions: + + * Some systems have legacy hashes that were generated using a different + character encoding. In this case, all :class:`!unicode` passwords + should be encoded using the correct encoding before they are hashed; + otherwise non-ASCII passwords may not :meth:`!verify` successfully. + + * For historical reasons, :class:`~passlib.hash.lmhash` uses ``cp437`` + as it's default encoding. It will handle :class:`!unicode` correctly; + but non-ASCII passwords provided as :class:`!bytes` must either be encoded + using ``"cp437"``, or :class:`!lmhash`'s ``encoding`` keyword must + be set to indicate which encoding was used. + +.. _crypt-methods: + +.. rst-class:: html-toggle + +Crypt Methods +============= +Taken together, the :meth:`~PasswordHash.genconfig` and :meth:`~PasswordHash.genhash` +are two tightly-coupled methods that mimic the standard Unix +"crypt" interface. The first method generates salt / configuration +strings from a set of settings, and the second hashes the password +using the provided configuration string. + +.. seealso:: + + Most applications will find :meth:`~PasswordHash.encrypt` much more useful, + as it combines the functionality of these two methods into one. + +.. classmethod:: PasswordHash.genconfig(\*\*setting_kwds) + + Returns a configuration string encoding settings for hash generation. + + This function takes in all the same :attr:`~PasswordHash.setting_kwds` + as :meth:`~PasswordHash.encrypt`, fills in suitable defaults, + and encodes the settings into a single "configuration" string, + suitable passing to :meth:`~PasswordHash.genhash`. + + :param \*\*kwds: + All additional keywords are algorithm-specific, and will be listed + in that hash's documentation; though many of the more common keywords + are listed under :attr:`~PasswordHash.setting_kwds` + Examples of common keywords include ``salt`` and ``rounds``. -Application Methods -=================== -The :meth:`~PasswordHash.encrypt`, :meth:`~PasswordHash.identify`, and :meth:`~PasswordHash.verify` methods are designed -to provide an easy interface for applications. They allow encrypt new passwords -without having to deal with details such as salt generation, verifying -passwords without having to deal with hash comparison rules, and determining -which scheme a hash belongs to when multiple schemes are in use. + :returns: + A configuration string (as :class:`!str`), or ``None`` if the scheme + does not support a separate configuration. -.. classmethod:: PasswordHash.encrypt(secret, \*\*settings_and_context_kwds) + :raises ValueError, TypeError: + This function raises exceptions for the same + reasons as :meth:`~PasswordHash.encrypt`. - encrypt secret, returning resulting hash string. + .. note:: + This configuration string is typically the same as the full hash string, + except that it lacks the final portion containing the digested password. + This is sometimes referred to as a "salt" string, though it typically + contains much more than just the salt parameter. + +.. classmethod:: PasswordHash.genhash(secret, config, \*\*context_kwds) + + Encrypt secret using specified configuration string. + + This takes in a password and a configuration string, + and returns a hash for that password. + + :type secret: unicode or bytes :arg secret: - A string containing the secret to encode. + string containing the password to be encrypted. - Unicode behavior is specified on a per-hash basis, - but the common case is to encode into utf-8 - before processing. - - :param \*\*settings_and_context_kwds: - All other keywords are algorithm-specified, - and should be listed in :attr:`~PasswordHash.setting_kwds` - and :attr:`~PasswordHash.context_kwds`. + :type config: unicode or bytes or ``None`` + :arg config: + configuration string to use when encrypting secret. + this can either be an existing hash that was previously + returned by :meth:`~PasswordHash.genhash`, or a configuration string + that was previously created by :meth:`~PasswordHash.genconfig`. - Common settings keywords include ``salt`` and ``rounds``. + ``None`` is accepted *only* for the hashes which lack a configuration + string (for which :meth:`~PasswordHash.genconfig` always returns ``None``). - :raises ValueError: - * if settings are invalid and handler cannot correct them. - (eg: if a ``salt`` string is to short, this will - cause an error; but a ``rounds`` value that's too large - should be silently clipped). - - * if a context keyword contains an invalid value, or was required - but omitted. - - * if secret contains forbidden characters (e.g: des-crypt forbids null characters). - this should rarely occur, since most modern algorithms have no limitations - on the types of characters. + :param \*\*kwds: + Very few hashes will have additional keywords. - :raises TypeError: if :samp:`{secret}` is not a bytes or unicode instance. + The ones that do typically require external contextual information + in order to calculate the digest. For these hashes, + the values must match the ones passed to the original + :meth:`~PasswordHash.encrypt` call when the hash was generated, + or the password will not verify. + + These additional keywords are algorithm-specific, and will be listed + in that hash's documentation; though the more common keywords + are listed under ::attr:`~PasswordHash.context_kwds`. + Examples of common keywords include ``user``. :returns: - Hash string, using an algorithm-specific format. + Encoded hash matching specified secret, config, and kwds. + This will always be a native :class:`!str` instance. + + :raises ValueError, TypeError: + This function raises exceptions for the same + reasons as :meth:`~PasswordHash.encrypt`. + + .. warning:: + + Traditionally, password verification using the "crypt" interface + was done by testing if ``hash == genhash(password, hash)``. + This test is only reliable for a handful of algorithms, + as various hash representation issues may cause false results. + Applications are strongly urged to use :meth:`~PasswordHash.verify` instead. + +.. _support-methods: + +Support Methods +=============== +There is currently one additional support method, :meth:`~PasswordHash.identify`: .. classmethod:: PasswordHash.identify(hash) - identify if a hash string belongs to this algorithm. + Quickly identify if a hash string belongs to this algorithm. + :type hash: unicode or bytes :arg hash: the candidate hash string to check :returns: - * ``True`` if input appears to be a hash string belonging to this algorithm. - * ``True`` if input appears to be a configuration string belonging to this algorithm. - * ``False`` if no input is an empty string or ``None``. - * ``False`` if none of the above conditions was met. + * ``True`` if the input is a configuration string or hash string + identifiable as belonging to this scheme (even if it's malformed). + * ``False`` if the input does not belong to this scheme. + + :raises TypeError: + if :samp:`{hash}` is not a unicode or bytes instance. .. note:: - The goal of this method is positively identify the correct - handler for a given hash, and do it as efficiently as possible. - In order to accomplish this, many implementations perform only minimal - validation of the candidate hashes. Thus, they may return ``True`` - for hashes which are identifiable, but malformed enough that - a :exc:`ValueError` is raised when the string is passed to - :func:`~PasswordHash.verify` or :func:`~PasswordHash.genhash`. - Because of this, applications should rely on this method only for identification, - not confirmation that a hash is correctly formed. + Hashes which lack a reliable method of identification may incorrectly + identify each-other's hashes (e.g. both :class:`~passlib.hash.lmhash` + and :class:`~passlib.hash.nthash` hash consist 32 hexidecimal characters). + + .. seealso:: + + If you are considering using this method to select from multiple + algorithms in order to verify a password, you may be better served + by the :ref:`CryptContext ` class. -.. classmethod:: PasswordHash.verify(secret, hash, \*\*context_kwds) +.. + the undocumented and experimental support methods currently include + parsehash() and bitsize() - verify a secret against an existing hash. +.. _informational-attributes: - This checks if a secret matches against the one stored - inside the specified hash. +Informational Attributes +======================== - :param secret: - A string containing the secret to check. - :param hash: - A string containing the hash to check against. +.. _general-attributes: + +General Information +------------------- +Each hash provides a handful of informational attributes, allowing +programs to dynamically adapt to the requirements of different +hash algorithms. The following attributes should be defined for all +the hashes in passlib: - :param \*\*context_kwds: - Any additional keywords will be passed to the encrypt - method. These should be limited to those listed - in :attr:`~PasswordHash.context_kwds`. +.. attribute:: PasswordHash.name - :raises TypeError: if :samp:`{secret}` is not a bytes or unicode instance. + Name uniquely identifying this hash. - :raises ValueError: - * if the hash not specified - * if the hash does not match this algorithm's hash format - * if the provided secret contains forbidden characters (see :meth:`~PasswordHash.encrypt`) + For the hashes built into Passlib, this will always match + the location where it was imported from — :samp:`passlib.hash.{name}` — + though externally defined hashes may not adhere to this. - :returns: - ``True`` if the secret matches, otherwise ``False``. + This should always be a :class:`!str` consisting of lowercase ``a-z``, + the digits ``0-9``, and the underscore character ``_``. -.. _crypt-methods: +.. attribute:: PasswordHash.setting_kwds -Crypt Methods -============= -While the application methods are generally the most useful when integrating -password support into an application, those methods are for the most part -built on top of the crypt interface, which is somewhat simpler -for *implementing* new password schemes. It also happens to match -more closely with the crypt api of most Unix systems, -and consists of two functions: :meth:`~PasswordHash.genconfig` -and :meth:`~PasswordHash.genhash`. - -.. classmethod:: PasswordHash.genconfig(\*\*settings_kwds) - - returns configuration string encoding settings for hash generation - - Many hashes have configuration options, and support a format - which encodes them into a single configuration string. - (This configuration string is usually an abbreviated version of their - encoded hash format, sans the actual checksum, and is commonly - referred to as a ``salt string``, though it may contain much more - than just a salt). - - This function takes in optional configuration options (a complete list - of which should be found in :attr:`~PasswordHash.setting_kwds`), validates - the inputs, fills in defaults where appropriate, and returns - a configuration string. - For algorithms which do not have any configuration options, - this function should always return ``None``. - - While each algorithm may have it's own configuration options, - the following keywords (if supported) should always have a consistent - meaning: - - * ``salt`` - algorithm uses a salt. if passed into genconfig, - should contain an encoded salt string of length and character set - required by the specific handler. - - salt strings which are too small or have invalid characters - should cause an error, salt strings which are too large - should be truncated but accepted. - - * ``rounds`` - algorithm uses a variable number of rounds. if passed - into genconfig, should contain an integer number of rounds - (this may represent logarithmic rounds, eg bcrypt, or linear, eg sha-crypt). - if the number of rounds is too small or too large, it should - be clipped but accepted. - - :param \*\*settings_kwds: - this function takes in keywords as specified in :attr:`~PasswordHash.setting_kwds`. - commonly supported keywords include ``salt`` and ``rounds``. + Tuple listing the keywords supported by :meth:`~PasswordHash.encrypt` + and :meth:`~PasswordHash.genconfig` that control hash generation, and which will + be encoded into the resulting hash. + + This list commonly includes keywords for controlling salt generation, + adjusting time-cost parameters, etc. Most of these settings are optional, + and suitable defaults will be chosen if they are omitted (e.g. salts + will be autogenerated). + + While the documentation for each hash should have a complete list of + the specific settings the hash uses, the following keywords should have + roughly the same behavior for all the hashes that support them: - :raises ValueError: - * if any configuration options are required, missing, AND - a default value cannot be autogenerated. - (for example: salt strings should be autogenerated if not specified). - * if any configuration options are invalid, and cannot be - normalized in a reasonble manner (eg: salt strings clipped to maximum size). + .. index:: + single: salt; PasswordHash keyword - :returns: - the configuration string, or ``None`` if the algorithm does not support any configuration options. + ``salt`` + Specifies a fixed salt string to use, rather than randomly + generating one. -.. classmethod:: PasswordHash.genhash(secret, config, \*\*context_kwds) + This option is supported by most of the hashes in Passlib, + though typically it isn't used, as random generation of a salt + is usually the desired behavior. + + Hashes typically require this to be a :class:`!unicode` or + :class:`!bytes` instance, with additional constraints + appropriate to the algorithm. - encrypt secret to hash + .. index:: + single: salt_size; PasswordHash keyword - takes in a password, optional configuration string, - and any required contextual information the algorithm needs, - and returns the encoded hash strings. + ``salt_size`` - :arg secret: string containing the password to be encrypted - :arg config: - configuration string to use when encrypting secret. - this can either be an existing hash that was previously - returned by :meth:`~PasswordHash.genhash`, or a configuration string - that was previously created by :meth:`~PasswordHash.genconfig`. + Most algorithms which support the ``salt`` setting will + autogenerate a salt when none is provided. Most of those hashes + will also offer this option, which allows the caller to specify + the size of salt which should be generated. If omitted, + the hash's default salt size will be used. - :param \*\*context_kwds: - All other keywords must be external contextual information - required by the algorithm to create the hash. If any, - these kwds must be specified in :attr:`~PasswordHash.context_kwds`. + .. seealso:: the :ref:`salt info ` attributes (below) - :raises TypeError: - * if the configuration string is not provided - * if required contextual information is not provided - * if :samp:`{secret}` is not a bytes or unicode instance. + .. index:: + single: rounds; PasswordHash keyword - :raises ValueError: - * if the configuration string is not in a recognized format. - * if the secret contains a forbidden character (rare, but some algorithms have limitations, eg: forbidding null characters) - * if the contextual information is invalid + ``rounds`` + If present, this means the hash can vary the number + of internal rounds used in some part of it's algorithm, + allowing the calculation to take a variable amount of processor + time, for increased security. - :returns: - encoded hash matching specified secret, config, and context. + While this is almost always a non-negative integer, + additional constraints may be present for each algorithm + (such as the cost varying on a linear or logarithmic scale). + + This value is typically omitted, in which case a default + value will be used. The defaults for all the hashes in Passlib + are periodically retuned to strike a balance between + security and responsiveness. + + .. seealso:: the :ref:`rounds info ` attributes (below) + + .. index:: + single: ident; PasswordHash keyword + + ``ident`` + If present, the class supports multiple formats for encoding + the same hash. The class's documentation will generally list + the allowed values, allowing alternate output formats to be selected. + + Note that these values will typically correspond to different + revision of the hash algorithm itself, and they may not all + offer the same level of security. + + .. index:: + single: relaxed; PasswordHash keyword + + .. _relaxed-keyword: + + ``relaxed`` + By default, passing :meth:`~PasswordHash.encrypt` an invalid + value will result in a :exc:`ValueError`. However, if ``relaxed=True``, + Passlib will attempt to correct the error, and if successful, + issue a :exc:`~passlib.exc.PasslibHashWarning` instead. + This warning may then be filtered if desired. + Correctable errors include (but aren not limited to): ``rounds`` + and ``salt_size`` values that are too low or too high, ``salt`` + strings that are too large, etc. + + This option is supported by most of the hashes in Passlib. + +.. attribute:: PasswordHash.context_kwds + + Tuple listing the keywords supported by :meth:`~PasswordHash.encrypt`, + :meth:`~PasswordHash.verify`, and :meth:`~PasswordHash.genhash` affect the hash, but are + not encoded within it, and thus must be provided each time + the hash is calculated. + + This list commonly includes a user account, http realm identifier, + etc. Most of these keywords are required by the hashes which support them, + as they are frequently used in place of an embedded salt parameter. + This is typically an empty tuple for most of the hashes in passlib. + + While the documentation for each hash should have a complete list of + the specific context keywords the hash uses, + the following keywords should have roughly the same behavior + for all the hashes that support them: + + .. index:: + single: user; PasswordHash keyword -.. _optional-attributes: + ``user`` + + If present, the class requires a username be specified whenever + performing a hash calculation (e.g. + :class:`~passlib.hash.postgres_md5` and + :class:`~passlib.hash.oracle10`). + + .. index:: + single: encoding; PasswordHash keyword + + ``encoding`` + + Some hashes have poorly-defined or host-dependant unicode behavior, + and properly hashing a non-ASCII password requires providing + the correct encoding (:class:`~passlib.hash.lmhash` is perhaps the worst offender). + Hashes which provide this keyword will always expose + their default encoding programmatically via the + :attr:`~PasswordHash.default_encoding` attribute. + +.. _salt-attributes: + +Salt Information +---------------- +For schemes which support a salt string, +``"salt"`` should be listed in their :attr:`~PasswordHash.setting_kwds`, +and the following attributes should be defined: + +.. attribute:: PasswordHash.max_salt_size -Optional Attributes -================================= -Many of the handlers expose the following informational -attributes (though their presence is not uniform or required -as of this version of Passlib). + The maximum number of bytes/characters allowed in the salt. + Should either be a positive integer, or ``None`` (indicating + the algorithm has no effective upper limit). -.. todo:: +.. attribute:: PasswordHash.min_salt_size + + The minimum number of bytes/characters required for the salt. + Must be an integer between 0 or :attr:`~PasswordHash.max_salt_size`. - Consider making these attributes required for all hashes - which support the appropriate keyword in :attr:`~PasswordHash.setting_kwds`. +.. attribute:: PasswordHash.default_salt_size -.. _optional-rounds-attributes: + The default salt size that will be used when generating a salt, + assuming ``salt_size`` is not set explicitly. This is typically + the same as :attr:`max_salt_size`, + or a sane default if ``max_salt_size=None``. + +.. attribute:: PasswordHash.salt_chars + + A unicode string containing all the characters permitted + in a salt string. + + For most :ref:`modular-crypt-format` hashes, + this is equal to :data:`passlib.utils.HASH64_CHARS`. + For the rare hashes where the ``salt`` parameter must be specified + in bytes, this will be a placeholder :class:`!bytes` object containing + all 256 possible byte values. + +.. + not yet documentated, want to make sure this is how we want to do things: + + .. attribute:: PasswordHash.default_salt_chars + + sequence of characters used to generate new salts. + this is typically the same as :attr:`~PasswordHash.salt_chars`, but some + hashes accept a larger-than-useful range, and this will + contain only the "common" values used for generation. + +.. _rounds-attributes: Rounds Information ------------------ -For schemes which support a variable number of rounds (ie, ``'rounds' in PasswordHash.setting_kwds``), -the following attributes are usually exposed. -(Applications can test for this suites' presence by using :func:`~passlib.utils.has_rounds_info`) +For schemes which support a variable number of iterations to adjust their time-cost, +``"rounds"`` should be listed in :attr:`~PasswordHash.setting_kwds`, +and the following attributes should be defined: .. attribute:: PasswordHash.max_rounds The maximum number of rounds the scheme allows. - Specifying values above this will generally result - in a warning, and :attr:`~!PasswordHash.max_rounds` will be used instead. - Must be a positive integer. + Specifying a value beyond this will result in a :exc:`ValueError`. + Will be a positive integer, or ``None`` (indicating + the algorithm has no effective upper limit). .. attribute:: PasswordHash.min_rounds The minimum number of rounds the scheme allows. - Specifying values below this will generally result - in a warning, and :attr:`~!PasswordHash.min_rounds` will be used instead. - Must be within ``range(0, max_rounds+1)``. + Specifying a value below this will result in a :exc:`ValueError`. + Will always be an integer between 0 and :attr:`~PasswordHash.max_rounds`. .. attribute:: PasswordHash.default_rounds - The default number of rounds that will be used if not - explicitly set when calling :meth:`~PasswordHash.encrypt` or :meth:`~PasswordHash.genconfig`. - Must be within ``range(min_rounds, max_rounds+1)``. + The default number of rounds that will be used if none is explicitly + provided to :meth:`~PasswordHash.encrypt`. + This will always be an integer between :attr:`~PasswordHash.min_rounds` + and :attr:`~PasswordHash.max_rounds`. .. attribute:: PasswordHash.rounds_cost - Specifies how the rounds value affects the amount of time taken. - Currently used values are: + While the cost parameter ``rounds`` is an integer, how it corresponds + to the amount of time taken can vary between hashes. This attribute + indicates the scale used by the hash: + + * ``"linear"`` - time taken scales linearly with rounds value + (e.g. :class:`~passlib.hash.sha512_crypt`) + * ``"log2"`` - time taken scales exponentially with rounds value + (e.g. :class:`~passlib.hash.bcrypt`) - ``linear`` - time taken scales linearly with rounds value (eg: :class:`~passlib.hash.sha512_crypt`) +.. + todo: haven't decided if this is how I want the api look before + formally publishing it in the documentation: - ``log2`` - time taken scales exponentially with rounds value (eg: :class:`~passlib.hash.bcrypt`) + .. _password-hash-backends: -.. _optional-salt-attributes: + Multiple Backends + ================= + .. note:: -Salt Information ----------------- -For schemes which support a salt (ie, ``'salt' in PasswordHash.setting_kwds``), -the following attributes are usually exposed. -(Applications can test for this suites' presence by using :func:`~passlib.utils.has_salt_info`) + For the most part, applications will not need this interface, + outside of perhaps calling the :meth:`~PasswordHash.get_backend` + to determine which the active backend. -.. attribute:: PasswordHash.max_salt_size + Some hashes provided by Passlib have multiple backends which they + select from at runtime, to provide the fastest implementation available. + Algorithms which offer multiple backends will expose the following + methods and attributes: - maximum number of characters which will be used - if a salt string is provided to :meth:`~PasswordHash.genconfig` or :meth:`~PasswordHash.encrypt`. - must be one of: + .. attribute:: PasswordHash.backends - * A positive integer - it should accept and silently truncate - any salt strings longer than this size. + Tuple listing names of potential backends (which may or may not be available). + If this attribute is not present, the hash does not support + multiple backends. - * ``None`` - the scheme should use all characters of a provided salt, - no matter how large. + While the names of the backends are specific to the hash algorithm, + the following standard names may be present: -.. attribute:: PasswordHash.min_salt_size + * ``"os_crypt"`` - backend which uses stdlib's :mod:`!crypt` module. + this backend will not be available if the underlying host OS + does not support the particular hash algorithm. - minimum number of characters required for any salt string - provided to :meth:`~PasswordHash.genconfig` or :meth:`~PasswordHash.encrypt`. - must be an integer within ``range(0,max_salt_size+1)``. + * ``"builtin"`` - backend using pure-python implementation built into + Passlib. All hashes will have this as their last backend, as a fallback. -.. attribute:: PasswordHash.default_salt_size + .. method:: PasswordHash.get_backend() - size of salts generated by genconfig - when no salt is provided by caller. - for most hashes, this defaults to :attr:`~PasswordHash.max_salt_size`. - this value must be within ``range(min_salt_size, max_salt_size+1)``. + This method should return the name of the currently active backend + that will be used by :meth:`!encrypt` and :meth:`!verify`. -.. attribute:: PasswordHash.salt_chars + :raises passlib.exc.MissingBackendError: + in the rare case that *no* backends can be loaded. - string containing list of all characters which are allowed - to be specified in salt parameter. - for most :ref:`MCF ` hashes, - this is equal to :data:`passlib.utils.h64.CHARS`. - - this must be a :class:`!unicode` string if the salt is encoded, - or (rarely) :class:`!bytes` if the salt is manipulating as unencoded raw bytes. - -.. todo:: - - This section lists the behavior for handlers which accept - salt strings containing encoded characters. - Some handlers may instead expect raw bytes for their salt keyword, - and handle encoding / decoding them internally. - It should be documented how these attributes - behave in that situation. + .. method:: PasswordHash.has_backend(backend) -.. - not yet documentated, want to make sure this is how we want to do things: + This method can be used to test if a specific backend is available. + Returns ``True`` or ``False``. - .. attribute:: PasswordHash.default_salt_chars + .. method:: PasswordHash.set_backend(backend) - sequence of characters used to generated new salts - when no salt is provided by caller. - for most hashes, this is the same as :attr:`!PasswordHash.salt_chars`; - but some hashes accept a much larger range of values - than are typically used. This field allows - the full range to be accepted, while only - a select subset to be used for generation. + This method can be used to select a specific backend. + The ``backend`` argument must be one of the backends listed + in :attr:`~PasswordHash.backends`, or the special value ``"default"``. - xxx: what about a bits_per_salt_char or some such, so effective salt strength - can be compared? + :raises passlib.exc.MissingBackendError: + if the specified backend is not available. -.. _hash-unicode-behavior: +.. index:: rounds; choosing the right value -Unicode Behavior -================ +.. _rounds-selection-guidelines: -.. versionadded:: 1.5 +Choosing the right rounds value +=============================== +Passlib's default rounds settings attempt to be secure enough for +the average [#avgsys]_ system. But the "right" value for a given hash +is dependant on the server, it's cpu, it's expected load, and it's users. +Since larger values mean increased work for an attacker, +**the right** ``rounds`` **value for a given server should be the largest +possible value that doesn't cause intolerable delay for your users**. +For most public facing services, you can generally have signin +take upwards of 250ms - 400ms before users start getting annoyed. +For superuser accounts, it should take as much time as the admin can stand +(usually ~4x more delay than a regular account). -Quick summary -------------- -For the application developer in a hurry: - -* Passwords should be provided as :class:`unicode` if possible. - While they may be provided as :class:`bytes`, - in that case it is strongly suggested - they be encoded using ``utf-8`` or ``ascii``. - -* Passlib will always return hashes as native python strings. - This means :class:`unicode` under Python 3, - and ``ascii``-encoded :class:`bytes` under Python 2. - -* Applications should provide hashes as :class:`unicode` if possible. - However, ``ascii``-encoded :class:`bytes` are also accepted - under Python 2. - -The following sections detail the issues surrounding -encoding password hashes, and the behavior required -by handlers implementing this API. -It can be skipped by the uninterested. - -Passwords ---------- -Applications are strongly encouraged to provide passwords -as :class:`unicode`. Two situations where an application -might need to provide a password as :class:`bytes`: -the application isn't unicode aware (lots of python 2 apps), -or it needs to verify a password hash that used a specific encoding (eg ``latin-1``). -For either of these cases, application developers should consider -the following issues: - -* Most hashes in Passlib operate on a string of bytes. - For handlers implementing such hashes, - passwords provided as :class:`unicode` should be encoded to ``utf-8``, - and passwords provided as :class:`bytes` should be treated as opaque. - - A few of these hashes officially specify this behavior; - the rest have no preferred encoding at all, - so this was chosen as a sensible standard behavior. - Unless the underlying algorithm specifies an alternate policy, - handlers should always encode unicode to ``utf-8``. - -* Because of the above behavior for :class:`unicode` inputs, - applications which encode their passwords are urged - to use ``utf-8`` or ``ascii``, - so that hashes they generate with encoded bytes - will verify correctly if/when they start using unicode. - - Applications which need to verify existing hashes - using an alternate encoding such as ``latin-1`` - should be wary of this future "gotcha". - -* A few hashes operate on :class:`unicode` strings instead. - For handlers implementing such hashes: - passwords provided as :class:`unicode` should be handled as appropriate, - and passwords provided as :class:`bytes` should be treated as ``utf-8``, - and decoded. - - This behavior was chosen in order to be compatible with - the common case (above), combined with the fact - that applications should never need to use a specific - encoding with these hashes, as they are natively unicode. - - (The only hashes in Passlib like this are - :class:`~passlib.hash.oracle10` and :class:`~passlib.hash.nthash`) - -Hashes ------- -With the exception of plaintext passwords, -literally *all* of the hash formats surveyed by the Passlib authors -use only the characters found in 7-bit ``ascii``. -This has caused most password hashing code (in python and elsewhere) -to draw a very blurry line between :class:`unicode` and :class:`bytes`. -Because of that, the following behavior was dictated less -by design requirements, and more by compatibility -and ease of implementation issues: - -* Handlers should accept hashes as either :class:`unicode` or - as ``ascii``-encoded :class:`bytes`. - - This behavior allows applications to provide hashes - as unicode or as bytes, as they please; making - (among other things) migration to Python 3 easier. - - The primary exception to this is handlers implementing - plaintext passwords. The implementations in passlib generally - use ``utf-8`` to encode unicode passwords, - and reproduce existing passwords as opaque bytes. - -* Internally, it is recommended that handlers - operate on :class:`unicode` for parsing / formatting - purposes, and using :class:`bytes` only on decoded - data to be passed directly into their digest routine. - -* Handlers should return hashes as native python strings. - This means :class:`unicode` under Python 3, - and ``ascii``-encoded :class:`bytes` under Python 2. - - This behavior was chosen to fit with Python 3's - unicode-oriented philosophy, while retaining - backwards compatibility with Passlib 1.4 and earlier - under Python 2. - - Handlers should use the :func:`passlib.utils.to_hash_str` function - to coerce their unicode hashes to whatever is appropriate - for the platform before returning them. - -.. rubric:: Footnotes - -.. [#otypes] While this specification is written referring to classes and classmethods, - password hash handlers can be any type of object (instance, module, etc), - so long as they offer attributes and functions with the required - signatures. For example, some of the handlers in Passlib are - instances of the :class:`~passlib.utils.handlers.PrefixWrapper` class. +Passlib's ``default_rounds`` values are retuned every major release (at a minimum) +by taking a rough estimate of what an "average" system is capable of, +and setting all the ``default_rounds`` values to take ~300ms on such a system. +However, some older algorithms (e.g. :class:`~passlib.hash.bsdi_crypt`) are weak enough that +a tradeoff must be made, choosing "secure but intolerably slow" over "fast but unacceptably insecure". +For this reason, it is strongly recommended to not use a value lower than Passlib's default. -.. [#supported] all supported password hashes, whether builtin or registered - from an external source can be found in the :mod:`passlib.hash` module. +.. [#avgsys] For Passlib 1.6, all hashes were retuned to take ~250ms on a + system with a 3 ghz 64 bit CPU. diff -Nru passlib-1.5.3/docs/requirements.txt passlib-1.6.1/docs/requirements.txt --- passlib-1.5.3/docs/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/docs/requirements.txt 2012-07-31 22:14:07.000000000 +0000 @@ -0,0 +1 @@ +cloud_sptheme>=1.4 diff -Nru passlib-1.5.3/passlib/__init__.py passlib-1.6.1/passlib/__init__.py --- passlib-1.5.3/passlib/__init__.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/__init__.py 2012-08-02 19:26:46.000000000 +0000 @@ -1,3 +1,3 @@ """passlib - suite of password hashing & generation routinges""" -__version__ = '1.5.3' +__version__ = '1.6.1' diff -Nru passlib-1.5.3/passlib/_setup/__init__.py passlib-1.6.1/passlib/_setup/__init__.py --- passlib-1.5.3/passlib/_setup/__init__.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/_setup/__init__.py 2012-05-03 16:36:58.000000000 +0000 @@ -1,5 +1 @@ -"""passlib.setup - helpers used by passlib's setup.py script - -note that unlike the rest of passlib, the code in this package must -work *unaltered* under both python 2 & 3 -""" +"""passlib.setup - helpers used by passlib's setup.py script""" diff -Nru passlib-1.5.3/passlib/_setup/cond2to3.py passlib-1.6.1/passlib/_setup/cond2to3.py --- passlib-1.5.3/passlib/_setup/cond2to3.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/_setup/cond2to3.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,152 +0,0 @@ -"""passlib.setup.cond2to3 - moneypatches 2to3 to provide conditional macros, ala SQLAlchemy""" -#========================================================= -#imports -#========================================================= -#core -from lib2to3.refactor import RefactoringTool -import re -#site -#local -__all__ = [ - "patch2to3", -] - -#========================================================= -#macro preprocessor -#========================================================= -py3k_start_re = re.compile(r"^(\s*)# Py3K #", re.I) -py3k_stop_re = re.compile(r"^(\s*)# end Py3K #", re.I) - -py2k_start_re = re.compile(r"^(\s*)# Py2K #", re.I) -py2k_stop_re = re.compile(r"^(\s*)# end Py2K #", re.I) - -bare_comment_re = re.compile(r"^(\s*)#(.*)") -bare_re = re.compile(r"^(\s*)(.*)") - -def preprocess(data, name): - #TODO: add flag so this can also function in reverse, for 3to2 - changed = False - - lines = data.split("\n") - state = 0 - #0: parsing normally, looking for start-p3k or start-py2k - #1: in Py3K block - removing comment chars until end-py3k - #2: in Py2K block - adding comment chars until end-py2k - idx = 0 - indent = '' - while idx < len(lines): - line = lines[idx] - - #hack to detect ''"abc" strings - using this as py25-compat way to indicate bytes. - #should really turn into a proper fixer. - #also, this check is really weak, and might fail in some cases - if '\'\'".*"' in line: - line = lines[idx] = line.replace("''", "b") - changed = True - - #check for py3k start marker - m = py3k_start_re.match(line) - if m: - if state in (0,2): - ident = m.group(1) - state = 1 - idx += 1 - continue - #states 1 this is an error... - raise SyntaxError("unexpected py3k-start marker on line %d of %r: %r" % (idx, name, line)) - - #check for py3k stop marker - if py3k_stop_re.match(line): - if state == 1: - state = 0 - idx += 1 - continue - #states 0,2 this is an error... - raise SyntaxError("unexpected py3k-stop marker on line %d of %r: %r" % (idx, name, line)) - - #check for py2k start marker - m = py2k_start_re.match(line) - if m: - if state in (0,1): - ident = m.group(1) - state = 2 - idx += 1 - continue - #states 2 this is an error... - raise SyntaxError("unexpected py2k-start marker on line %d of %r: %r" % (idx, name, line)) - - #check for py2k end marker - if py2k_stop_re.match(line): - if state == 2: - state = 0 - idx += 1 - continue - #states 0,1 this is an error... - raise SyntaxError("unexpected py2k-stop marker on line %d of %r: %r" % (idx, name, line)) - - #state 0 - leave non-marker lines alone - if state == 0: - idx += 1 - continue - - #state 1 - uncomment comment lines, throw error on bare lines - if state == 1: - m = bare_comment_re.match(line) - if not m: - raise SyntaxError("unexpected non-comment in py3k block on line %d of %r: %r" % (idx,name, line)) - pad, content = m.group(1,2) - lines[idx] = pad + content - changed = True - idx += 1 - continue - - #state 2 - comment out all lines - if state == 2: - m = bare_re.match(line) - if not m: - raise RuntimeError("unexpected failure to parse line %d of %r: %r" % (idx, name, line)) - pad, content = m.group(1,2) - if pad.startswith(ident): #try to put comments on same level - content = pad[len(ident):] + content - pad = ident - lines[idx] = "%s#%s" % (pad,content) - changed = True - idx += 1 - continue - - #should never get here - raise AssertionError("invalid state: %r" % (state,)) - - if changed: - return "\n".join(lines) - else: - return data - -orig_rs = RefactoringTool.refactor_string - -def refactor_string(self, data, name): - "replacement for RefactoringTool.refactor_string which honors conditional includes" - newdata = preprocess(data, name) - tree = orig_rs(self, newdata, name) - if tree and newdata != data: - tree.was_changed = True - return tree - -#========================================================= -#main -#========================================================= - -def patch2to3(): - "frontend to patch preprocessor into lib2to3" - RefactoringTool.refactor_string = refactor_string - -#helper for development purposes - runs 2to3 w/ patch -if __name__ == "__main__": - import sys - from lib2to3.main import main - patch2to3() - sys.exit(main("lib2to3.fixes")) - -#========================================================= -#eof -#========================================================= diff -Nru passlib-1.5.3/passlib/_setup/docdist.py passlib-1.6.1/passlib/_setup/docdist.py --- passlib-1.5.3/passlib/_setup/docdist.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/_setup/docdist.py 2012-08-01 17:05:50.000000000 +0000 @@ -1,35 +1,35 @@ "custom command to build doc.zip file" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core import os from distutils import dir_util from distutils.cmd import Command from distutils.errors import * from distutils.spawn import spawn -#local +# local __all__ = [ "docdist" ] -#========================================================= -#command -#========================================================= +#============================================================================= +# command +#============================================================================= class docdist(Command): description = "create zip file containing standalone html docs" - + user_options = [ ('build-dir=', None, 'Build directory'), ('dist-dir=', 'd', "directory to put the source distribution archive(s) in " - "[default: dist]"), + "[default: dist]"), ('format=', 'f', "archive format to create (tar, ztar, gztar, zip)"), ('sign', 's', 'sign files using gpg'), ('identity=', 'i', 'GPG identity used to sign files'), ] - + def initialize_options(self): self.build_dir = None self.dist_dir = None @@ -37,7 +37,7 @@ self.keep_temp = False self.sign = False self.identity = None - + def finalize_options(self): if self.identity and not self.sign: raise DistutilsOptionError( @@ -50,7 +50,7 @@ self.dist_dir = "dist" if not self.format: self.format = "zip" - + def run(self): # call build sphinx to build docs self.run_command("build_sphinx") @@ -77,11 +77,11 @@ gpg_args[2:2] = ["--local-user", self.identity] spawn(gpg_args, dry_run=self.dry_run) - - #cleanup + + # cleanup if not self.keep_temp: dir_util.remove_tree(tmp_dir, dry_run=self.dry_run) - -#========================================================= -#eof -#========================================================= + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/_setup/stamp.py passlib-1.6.1/passlib/_setup/stamp.py --- passlib-1.5.3/passlib/_setup/stamp.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/_setup/stamp.py 2012-08-01 17:05:50.000000000 +0000 @@ -1,22 +1,22 @@ "update version string during build" -#========================================================= +#============================================================================= # imports -#========================================================= +#============================================================================= from __future__ import with_statement -#core +# core import os import re import time from distutils.dist import Distribution -#pkg -#local +# pkg +# local __all__ = [ "stamp_source", "stamp_distutils_output", ] -#========================================================= +#============================================================================= # helpers -#========================================================= +#============================================================================= def get_command_class(opts, name): return opts['cmdclass'].get(name) or Distribution().get_command_class(name) @@ -25,33 +25,33 @@ path = os.path.join(base_dir, "passlib", "__init__.py") with open(path) as fh: input = fh.read() - output = re.sub('(?m)^__version__\s*=.*$', + output, count = re.subn('(?m)^__version__\s*=.*$', '__version__ = ' + repr(version), input) - assert output != input, "failed to match" + assert count == 1, "failed to replace version string" if not dry_run: os.unlink(path) # sdist likes to use hardlinks with open(path, "w") as fh: fh.write(output) - + def stamp_distutils_output(opts, version): - + # subclass buildpy to update version string in source _build_py = get_command_class(opts, "build_py") class build_py(_build_py): def build_packages(self): _build_py.build_packages(self) - stamp_source(self.build_lib, version, self.dry_run) + stamp_source(self.build_lib, version, self.dry_run) opts['cmdclass']['build_py'] = build_py - + # subclass sdist to do same thing _sdist = get_command_class(opts, "sdist") - class sdist(_sdist): + class sdist(_sdist): def make_release_tree(self, base_dir, files): _sdist.make_release_tree(self, base_dir, files) stamp_source(base_dir, version, self.dry_run) opts['cmdclass']['sdist'] = sdist - -#========================================================= + +#============================================================================= # eof -#========================================================= +#============================================================================= diff -Nru passlib-1.5.3/passlib/apache.py passlib-1.6.1/passlib/apache.py --- passlib-1.5.3/passlib/apache.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/apache.py 2012-08-01 17:05:50.000000000 +0000 @@ -1,399 +1,773 @@ """passlib.apache - apache password support""" -#========================================================= -#imports -#========================================================= +# XXX: relocate this to passlib.ext.apache? +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core from hashlib import md5 import logging; log = logging.getLogger(__name__) import os import sys -#site -#libs +from warnings import warn +# site +# pkg from passlib.context import CryptContext -from passlib.utils import render_bytes, bjoin, bytes, b, to_unicode, to_bytes -#pkg -#local +from passlib.exc import ExpectedStringError +from passlib.hash import htdigest +from passlib.utils import consteq, render_bytes, to_bytes, deprecated_method, is_ascii_codec +from passlib.utils.compat import b, bytes, join_bytes, str_to_bascii, u, \ + unicode, BytesIO, iteritems, imap, PY3 +# local __all__ = [ + 'HtpasswdFile', + 'HtdigestFile', ] -BCOLON = b(":") +#============================================================================= +# constants & support +#============================================================================= +_UNSET = object() + +_BCOLON = b(":") + +# byte values that aren't allowed in fields. +_INVALID_FIELD_CHARS = b(":\n\r\t\x00") + +#============================================================================= +# backport of OrderedDict for PY2.5 +#============================================================================= +try: + from collections import OrderedDict +except ImportError: + # Python 2.5 + class OrderedDict(dict): + """hacked OrderedDict replacement. -#========================================================= -#common helpers -#========================================================= -DEFAULT_ENCODING = "utf-8" if sys.version_info >= (3,0) else None + NOTE: this doesn't provide a full OrderedDict implementation, + just the minimum needed by the Htpasswd internals. + """ + def __init__(self): + self._keys = [] + + def __iter__(self): + return iter(self._keys) + def __setitem__(self, key, value): + if key not in self: + self._keys.append(key) + super(OrderedDict, self).__setitem__(key, value) + + def __delitem__(self, key): + super(OrderedDict, self).__delitem__(key) + self._keys.remove(key) + + def iteritems(self): + return ((key, self[key]) for key in self) + + # these aren't used or implemented, so disabling them for safety. + update = pop = popitem = clear = keys = iterkeys = None + +#============================================================================= +# common helpers +#============================================================================= class _CommonFile(object): - "helper for HtpasswdFile / HtdigestFile" + """common framework for HtpasswdFile & HtdigestFile""" + #=================================================================== + # instance attrs + #=================================================================== + + # charset encoding used by file (defaults to utf-8) + encoding = None + + # whether users() and other public methods should return unicode or bytes? + # (defaults to False under PY2, True under PY3) + return_unicode = None + + # if bound to local file, these will be set. + _path = None # local file path + _mtime = None # mtime when last loaded, or 0 + + # if true, automatically save to local file after changes are made. + autosave = False + + # ordered dict mapping key -> value for all records in database. + # (e.g. user => hash for Htpasswd) + _records = None + + #=================================================================== + # alt constuctors + #=================================================================== + @classmethod + def from_string(cls, data, **kwds): + """create new object from raw string. - #XXX: would like to add 'path' keyword to load() / save(), - # but that makes .mtime somewhat meaningless. - # to simplify things, should probably deprecate mtime & force=False - # options. - #XXX: would also like to make _load_string available via public interface, - # such as via 'content' keyword in load() method. - # in short, need to clean up the htpasswd api a little bit in 1.6. - # keeping _load_string private for now, cause just using it for UTing. - - #NOTE: 'path' is a property instead of attr, - # so that .mtime is wiped whenever path is changed. - _path = None - def _get_path(self): - return self._path - def _set_path(self, path): - if path != self._path: - self.mtime = 0 - self._path = path - path = property(_get_path, _set_path) + :type data: unicode or bytes + :arg data: + database to load, as single string. + + :param \*\*kwds: + all other keywords are the same as in the class constructor + """ + if 'path' in kwds: + raise TypeError("'path' not accepted by from_string()") + self = cls(**kwds) + self.load_string(data) + return self @classmethod - def _from_string(cls, content, **kwds): - #NOTE: not public yet, just using it for unit tests. + def from_path(cls, path, **kwds): + """create new object from file, without binding object to file. + + :type path: str + :arg path: + local filepath to load from + + :param \*\*kwds: + all other keywords are the same as in the class constructor + """ self = cls(**kwds) - self._load_string(content) + self.load(path) return self - def __init__(self, path=None, autoload=True, - encoding=DEFAULT_ENCODING, + #=================================================================== + # init + #=================================================================== + def __init__(self, path=None, new=False, autoload=True, autosave=False, + encoding="utf-8", return_unicode=PY3, ): - if encoding and u":\n".encode(encoding) != b(":\n"): - #rest of file assumes ascii bytes, and uses ":" as separator. - raise ValueError, "encoding must be 7-bit ascii compatible" + # set encoding + if not encoding: + warn("``encoding=None`` is deprecated as of Passlib 1.6, " + "and will cause a ValueError in Passlib 1.8, " + "use ``return_unicode=False`` instead.", + DeprecationWarning, stacklevel=2) + encoding = "utf-8" + return_unicode = False + elif not is_ascii_codec(encoding): + # htpasswd/htdigest files assumes 1-byte chars, and use ":" separator, + # so only ascii-compatible encodings are allowed. + raise ValueError("encoding must be 7-bit ascii compatible") self.encoding = encoding - self.path = path - ##if autoload == "exists": - ## autoload = bool(path and os.path.exists(path)) - if autoload and path: + + # set other attrs + self.return_unicode = return_unicode + self.autosave = autosave + self._path = path + self._mtime = 0 + + # init db + if not autoload: + warn("``autoload=False`` is deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8, use ``new=True`` instead", + DeprecationWarning, stacklevel=2) + new = True + if path and not new: self.load() - ##elif raw: - ## self._load_lines(raw.split("\n")) else: - self._entry_order = [] - self._entry_map = {} + self._records = OrderedDict() - def _load_string(self, content): - """UT helper for loading from string - - to be improved/made public in later release. + def __repr__(self): + tail = '' + if self.autosave: + tail += ' autosave=True' + if self._path: + tail += ' path=%r' % self._path + if self.encoding != "utf-8": + tail += ' encoding=%r' % self.encoding + return "<%s 0x%0x%s>" % (self.__class__.__name__, id(self), tail) + # NOTE: ``path`` is a property so that ``_mtime`` is wiped when it's set. + def _get_path(self): + return self._path + def _set_path(self, value): + if value != self._path: + self._mtime = 0 + self._path = value + path = property(_get_path, _set_path) - :param content: - if specified, should be a bytes object. - passwords will be loaded directly from this string, - and any files will be ignored. - """ - if isinstance(content, unicode): - content = content.encode(self.encoding or 'utf-8') - self.mtime = 0 - #XXX: replace this with iterator? - lines = content.splitlines() - self._load_lines(lines) + @property + def mtime(self): + "modify time when last loaded (if bound to a local file)" + return self._mtime + + #=================================================================== + # loading + #=================================================================== + def load_if_changed(self): + """Reload from ``self.path`` only if file has changed since last load""" + if not self._path: + raise RuntimeError("%r is not bound to a local file" % self) + if self._mtime and self._mtime == os.path.getmtime(self._path): + return False + self.load() return True - def load(self, force=True): - """load entries from file + def load(self, path=None, force=True): + """Load state from local file. + If no path is specified, attempts to load from ``self.path``. - :param force: - if ``True`` (the default), always loads state from file. - if ``False``, only loads state if file has been modified since last load. + :type path: str + :arg path: local file to load from - :raises IOError: if file not found + :type force: bool + :param force: + if ``force=False``, only load from ``self.path`` if file + has changed since last load. - :returns: ``False`` if ``force=False`` and no load performed; otherwise ``True``. + .. deprecated:: 1.6 + This keyword will be removed in Passlib 1.8; + Applications should use :meth:`load_if_changed` instead. """ - path = self.path - if not path: - raise RuntimeError("no load path specified") - if not force and self.mtime and self.mtime == os.path.getmtime(path): - return False - with open(path, "rb") as fh: - self.mtime = os.path.getmtime(path) - self._load_lines(fh) + if path is not None: + with open(path, "rb") as fh: + self._mtime = 0 + self._load_lines(fh) + elif not force: + warn("%(name)s.load(force=False) is deprecated as of Passlib 1.6," + "and will be removed in Passlib 1.8; " + "use %(name)s.load_if_changed() instead." % + dict(name=self.__class__.__name__), + DeprecationWarning, stacklevel=2) + return self.load_if_changed() + elif self._path: + with open(self._path, "rb") as fh: + self._mtime = os.path.getmtime(self._path) + self._load_lines(fh) + else: + raise RuntimeError("%s().path is not set, an explicit path is required" % + self.__class__.__name__) return True + def load_string(self, data): + "Load state from unicode or bytes string, replacing current state" + data = to_bytes(data, self.encoding, "data") + self._mtime = 0 + self._load_lines(BytesIO(data)) + def _load_lines(self, lines): - pl = self._parse_line - entry_order = self._entry_order = [] - entry_map = self._entry_map = {} - for line in lines: - #XXX: found mention that "#" comment lines may be supported by htpasswd, - # should verify this. - key, value = pl(line) - if key in entry_map: - #XXX: should we use data from first entry, or last entry? - # going w/ first entry for now. - continue - entry_order.append(key) - entry_map[key] = value + "load from sequence of lists" + # XXX: found reference that "#" comment lines may be supported by + # htpasswd, should verify this, and figure out how to handle them. + # if true, this would also affect what can be stored in user field. + # XXX: if multiple entries for a key, should we use the first one + # or the last one? going w/ first entry for now. + # XXX: how should this behave if parsing fails? currently + # it will contain everything that was loaded up to error. + # could clear / restore old state instead. + parse = self._parse_record + records = self._records = OrderedDict() + for idx, line in enumerate(lines): + key, value = parse(line, idx+1) + if key not in records: + records[key] = value + + def _parse_record(cls, record, lineno): # pragma: no cover - abstract method + "parse line of file into (key, value) pair" + raise NotImplementedError("should be implemented in subclass") + + #=================================================================== + # saving + #=================================================================== + def _autosave(self): + "subclass helper to call save() after any changes" + if self.autosave and self._path: + self.save() + + def save(self, path=None): + """Save current state to file. + If no path is specified, attempts to save to ``self.path``. + """ + if path is not None: + with open(path, "wb") as fh: + fh.writelines(self._iter_lines()) + elif self._path: + self.save(self._path) + self._mtime = os.path.getmtime(self._path) + else: + raise RuntimeError("%s().path is not set, cannot autosave" % + self.__class__.__name__) - #subclass: _parse_line(line) -> (key, hash) + def to_string(self): + "Export current state as a string of bytes" + return join_bytes(self._iter_lines()) def _iter_lines(self): "iterator yielding lines of database" - rl = self._render_line - entry_order = self._entry_order - entry_map = self._entry_map - assert len(entry_order) == len(entry_map), "internal error in entry list" - return (rl(key, entry_map[key]) for key in entry_order) - - def save(self): - "save entries to file" - if not self.path: - raise RuntimeError("no save path specified") - with open(self.path, "wb") as fh: - fh.writelines(self._iter_lines()) - self.mtime = os.path.getmtime(self.path) - - def to_string(self): - "export whole database as a byte string" - return bjoin(self._iter_lines()) - - #subclass: _render_line(entry) -> line - - def _update_key(self, key, value): - entry_map = self._entry_map - if key in entry_map: - entry_map[key] = value - return True - else: - self._entry_order.append(key) - entry_map[key] = value - return False + return (self._render_record(key,value) for key,value in iteritems(self._records)) - def _delete_key(self, key): - entry_map = self._entry_map - if key in entry_map: - del entry_map[key] - self._entry_order.remove(key) - return True - else: - return False + def _render_record(cls, key, value): # pragma: no cover - abstract method + "given key/value pair, encode as line of file" + raise NotImplementedError("should be implemented in subclass") + + #=================================================================== + # field encoding + #=================================================================== + def _encode_user(self, user): + "user-specific wrapper for _encode_field()" + return self._encode_field(user, "user") + + def _encode_realm(self, realm): # pragma: no cover - abstract method + "realm-specific wrapper for _encode_field()" + return self._encode_field(realm, "realm") + + def _encode_field(self, value, param="field"): + """convert field to internal representation. + + internal representation is always bytes. byte strings are left as-is, + unicode strings encoding using file's default encoding (or ``utf-8`` + if no encoding has been specified). + + :raises UnicodeEncodeError: + if unicode value cannot be encoded using default encoding. + + :raises ValueError: + if resulting byte string contains a forbidden character, + or is too long (>255 bytes). - invalid_chars = b(":\n\r\t\x00") + :returns: + encoded identifer as bytes + """ + if isinstance(value, unicode): + value = value.encode(self.encoding) + elif not isinstance(value, bytes): + raise ExpectedStringError(value, param) + if len(value) > 255: + raise ValueError("%s must be at most 255 characters: %r" % + (param, value)) + if any(c in _INVALID_FIELD_CHARS for c in value): + raise ValueError("%s contains invalid characters: %r" % + (param, value,)) + return value + + def _decode_field(self, value): + """decode field from internal representation to format + returns by users() method, etc. + + :raises UnicodeDecodeError: + if unicode value cannot be decoded using default encoding. + (usually indicates wrong encoding set for file). - def _norm_user(self, user): - "encode user to bytes, validate against format requirements" - return self._norm_ident(user, errname="user") - - def _norm_realm(self, realm): - "encode realm to bytes, validate against format requirements" - return self._norm_ident(realm, errname="realm") - - def _norm_ident(self, ident, errname="user/realm"): - ident = self._encode_ident(ident, errname) - if len(ident) > 255: - raise ValueError("%s must be at most 255 characters: %r" % (errname, ident)) - if any(c in self.invalid_chars for c in ident): - raise ValueError("%s contains invalid characters: %r" % (errname, ident,)) - return ident - - def _encode_ident(self, ident, errname="user/realm"): - "ensure identifier is bytes encoded using specified encoding, or rejected" - encoding = self.encoding - if encoding: - if isinstance(ident, unicode): - return ident.encode(encoding) - raise TypeError("%s must be unicode, not %s" % - (errname, type(ident))) - else: - if isinstance(ident, bytes): - return ident - raise TypeError("%s must be bytes, not %s" % - (errname, type(ident))) - - def _decode_ident(self, ident, errname="user/realm"): - "decode an identifier (if encoding is specified, else return encoded bytes)" - assert isinstance(ident, bytes) - encoding = self.encoding - if encoding: - return ident.decode(encoding) + :returns: + field as unicode or bytes, as appropriate. + """ + assert isinstance(value, bytes), "expected value to be bytes" + if self.return_unicode: + return value.decode(self.encoding) else: - return ident + return value - #FIXME: htpasswd doc sez passwords limited to 255 chars under Windows & MPE, - # longer ones are truncated. may be side-effect of those platforms - # supporting plaintext. we don't currently check for this. - -#========================================================= -#htpasswd editing -#========================================================= -#FIXME: apr_md5_crypt technically the default only for windows, netware and tpf. -#TODO: find out if htpasswd's "crypt" mode is crypt *call* or just des_crypt implementation. + # FIXME: htpasswd doc says passwords limited to 255 chars under Windows & MPE, + # and that longer ones are truncated. this may be side-effect of those + # platforms supporting the 'plaintext' scheme. these classes don't currently + # check for this. + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htpasswd editing +#============================================================================= + +# FIXME: apr_md5_crypt technically the default only for windows, netware and tpf. +# TODO: find out if htpasswd's "crypt" mode is a crypt() *call* or just des_crypt implementation. +# if the former, we can support anything supported by passlib.hosts.host_context, +# allowing more secure hashes than apr_md5_crypt to be used. +# could perhaps add this behavior as an option to the constructor. +# c.f. http://httpd.apache.org/docs/2.2/programs/htpasswd.html htpasswd_context = CryptContext([ - "apr_md5_crypt", #man page notes supported everywhere, default on Windows, Netware, TPF - "des_crypt", #man page notes server does NOT support this on Windows, Netware, TPF - "ldap_sha1", #man page notes only for transitioning <-> ldap + "apr_md5_crypt", # man page notes supported everywhere, default on Windows, Netware, TPF + "des_crypt", # man page notes server does NOT support this on Windows, Netware, TPF + "ldap_sha1", # man page notes only for transitioning <-> ldap "plaintext" # man page notes server ONLY supports this on Windows, Netware, TPF ]) class HtpasswdFile(_CommonFile): """class for reading & writing Htpasswd files. - :arg path: path to htpasswd file to load from / save to (required) + The class constructor accepts the following arguments: - :param default: - optionally specify default scheme to use when encoding new passwords. + :type path: filepath + :param path: - Must be one of ``None``, ``"apr_md5_crypt"``, ``"des_crypt"``, ``"ldap_sha1"``, ``"plaintext"``. + Specifies path to htpasswd file, use to implicitly load from and save to. - If no value is specified, this class currently uses ``apr_md5_crypt`` when creating new passwords. + This class has two modes of operation: - :param autoload: - if ``True`` (the default), :meth:`load` will be automatically called - by constructor. + 1. It can be "bound" to a local file by passing a ``path`` to the class + constructor. In this case it will load the contents of the file when + created, and the :meth:`load` and :meth:`save` methods will automatically + load from and save to that file if they are called without arguments. + + 2. Alternately, it can exist as an independant object, in which case + :meth:`load` and :meth:`save` will require an explicit path to be + provided whenever they are called. As well, ``autosave`` behavior + will not be available. + + This feature is new in Passlib 1.6, and is the default if no + ``path`` value is provided to the constructor. + + This is also exposed as a readonly instance attribute. + + :type new: bool + :param new: + + Normally, if *path* is specified, :class:`HtpasswdFile` will + immediately load the contents of the file. However, when creating + a new htpasswd file, applications can set ``new=True`` so that + the existing file (if any) will not be loaded. + + .. versionadded:: 1.6 + This feature was previously enabled by setting ``autoload=False``. + That alias has been deprecated, and will be removed in Passlib 1.8 - Set to ``False`` to disable automatic loading (primarily used when - creating new htdigest file). + :type autosave: bool + :param autosave: + Normally, any changes made to an :class:`HtpasswdFile` instance + will not be saved until :meth:`save` is explicitly called. However, + if ``autosave=True`` is specified, any changes made will be + saved to disk immediately (assuming *path* has been set). + + This is also exposed as a writeable instance attribute. + + :type encoding: str :param encoding: - optionally specify encoding used for usernames. - if set to ``None``, - user names must be specified as bytes, - and will be returned as bytes. - - if set to an encoding, - user names must be specified as unicode, - and will be returned as unicode. - when stored, then will use the specified encoding. - - for backwards compatibility with passlib 1.4, - this defaults to ``None`` under Python 2, - and ``utf-8`` under Python 3. - - .. note:: - - this is not the encoding for the entire file, - just for the usernames within the file. - this must be an encoding which is compatible - with 7-bit ascii (which is used by rest of file). + Optionally specify character encoding used to read/write file + and hash passwords. Defaults to ``utf-8``, though ``latin-1`` + is the only other commonly encountered encoding. + + This is also exposed as a readonly instance attribute. + + :type default_scheme: str + :param default_scheme: + Optionally specify default scheme to use when encoding new passwords. + Must be one of ``"apr_md5_crypt"``, ``"des_crypt"``, ``"ldap_sha1"``, + ``"plaintext"``. It defaults to ``"apr_md5_crypt"``. + + .. versionadded:: 1.6 + This keyword was previously named ``default``. That alias + has been deprecated, and will be removed in Passlib 1.8. + :type context: :class:`~passlib.context.CryptContext` :param context: - :class:`~passlib.context.CryptContext` instance used to handle - hashes in this file. + :class:`!CryptContext` instance used to encrypt + and verify the hashes found in the htpasswd file. + The default value is a pre-built context which supports all + of the hashes officially allowed in an htpasswd file. + + This is also exposed as a readonly instance attribute. .. warning:: - this should usually be left at the default, - though it can be overridden to implement non-standard hashes - within the htpasswd file. + This option may be used to add support for non-standard hash + formats to an htpasswd file. However, the resulting file + will probably not be usuable by another application, + and particularly not by Apache. + + :param autoload: + Set to ``False`` to prevent the constructor from automatically + loaded the file from disk. + + .. deprecated:: 1.6 + This has been replaced by the *new* keyword. + Instead of setting ``autoload=False``, you should use + ``new=True``. Support for this keyword will be removed + in Passlib 1.8. + + :param default: + Change the default algorithm used to encrypt new passwords. + + .. deprecated:: 1.6 + This has been renamed to *default_scheme* for clarity. + Support for this alias will be removed in Passlib 1.8. Loading & Saving ================ .. automethod:: load + .. automethod:: load_if_changed + .. automethod:: load_string .. automethod:: save .. automethod:: to_string Inspection ================ .. automethod:: users - .. automethod:: verify + .. automethod:: check_password + .. automethod:: get_hash Modification ================ - .. automethod:: update + .. automethod:: set_password .. automethod:: delete - .. note:: + Alternate Constructors + ====================== + .. automethod:: from_string - All of the methods in this class enforce some data validation - on the ``user`` parameter: - they will raise a :exc:`ValueError` if the string - contains one of the forbidden characters ``:\\r\\n\\t\\x00``, + Attributes + ========== + .. attribute:: path + + Path to local file that will be used as the default + for all :meth:`load` and :meth:`save` operations. + May be written to, initialized by the *path* constructor keyword. + + .. attribute:: autosave + + Writeable flag indicating whether changes will be automatically + written to *path*. + + Errors + ====== + :raises ValueError: + All of the methods in this class will raise a :exc:`ValueError` if + any user name contains a forbidden character (one of ``:\\r\\n\\t\\x00``), or is longer than 255 characters. """ - def __init__(self, path=None, default=None, context=htpasswd_context, **kwds): + #=================================================================== + # instance attrs + #=================================================================== + + # NOTE: _records map stores for the key, and for the value, + # both in bytes which use self.encoding + + #=================================================================== + # init & serialization + #=================================================================== + def __init__(self, path=None, default_scheme=None, context=htpasswd_context, + **kwds): + if 'default' in kwds: + warn("``default`` is deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8, it has been renamed " + "to ``default_scheem``.", + DeprecationWarning, stacklevel=2) + default_scheme = kwds.pop("default") + if default_scheme: + context = context.copy(default=default_scheme) self.context = context - if default: - self.context = self.context.replace(default=default) super(HtpasswdFile, self).__init__(path, **kwds) - def _parse_line(self, line): - #should be user, hash - return line.rstrip().split(BCOLON) + def _parse_record(self, record, lineno): + # NOTE: should return (user, hash) tuple + result = record.rstrip().split(_BCOLON) + if len(result) != 2: + raise ValueError("malformed htpasswd file (error reading line %d)" + % lineno) + return result - def _render_line(self, user, hash): + def _render_record(self, user, hash): return render_bytes("%s:%s\n", user, hash) + #=================================================================== + # public methods + #=================================================================== + def users(self): - "return list of all users in file" - return map(self._decode_ident, self._entry_order) + "Return list of all users in database" + return [self._decode_field(user) for user in self._records] - def update(self, user, password): - """update password for user; adds user if needed. + ##def has_user(self, user): + ## "check whether entry is present for user" + ## return self._encode_user(user) in self._records + + ##def rename(self, old, new): + ## """rename user account""" + ## old = self._encode_user(old) + ## new = self._encode_user(new) + ## hash = self._records.pop(old) + ## self._records[new] = hash + ## self._autosave() - :returns: ``True`` if existing user was updated, ``False`` if user added. + def set_password(self, user, password): + """Set password for user; adds user if needed. + + :returns: + * ``True`` if existing user was updated. + * ``False`` if user account was added. + + .. versionchanged:: 1.6 + This method was previously called ``update``, it was renamed + to prevent ambiguity with the dictionary method. + The old alias is deprecated, and will be removed in Passlib 1.8. """ - user = self._norm_user(user) + user = self._encode_user(user) hash = self.context.encrypt(password) - return self._update_key(user, hash) + if PY3: + hash = hash.encode(self.encoding) + existing = (user in self._records) + self._records[user] = hash + self._autosave() + return existing + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="set_password") + def update(self, user, password): + "set password for user" + return self.set_password(user, password) + + def get_hash(self, user): + """Return hash stored for user, or ``None`` if user not found. + + .. versionchanged:: 1.6 + This method was previously named ``find``, it was renamed + for clarity. The old name is deprecated, and will be removed + in Passlib 1.8. + """ + try: + return self._records[self._encode_user(user)] + except KeyError: + return None + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="get_hash") + def find(self, user): + "return hash for user" + return self.get_hash(user) + + # XXX: rename to something more explicit, like delete_user()? def delete(self, user): - """delete user's entry. + """Delete user's entry. - :returns: ``True`` if user deleted, ``False`` if user not found. + :returns: + * ``True`` if user deleted. + * ``False`` if user not found. """ - user = self._norm_user(user) - return self._delete_key(user) + try: + del self._records[self._encode_user(user)] + except KeyError: + return False + self._autosave() + return True - def verify(self, user, password): - """verify password for specified user. + def check_password(self, user, password): + """Verify password for specified user. :returns: - * ``None`` if user not found - * ``False`` if password does not match - * ``True`` if password matches. + * ``None`` if user not found. + * ``False`` if user found, but password does not match. + * ``True`` if user found and password matches. + + .. versionchanged:: 1.6 + This method was previously called ``verify``, it was renamed + to prevent ambiguity with the :class:`!CryptContext` method. + The old alias is deprecated, and will be removed in Passlib 1.8. """ - user = self._norm_user(user) - hash = self._entry_map.get(user) + user = self._encode_user(user) + hash = self._records.get(user) if hash is None: return None - else: - return self.context.verify(password, hash) - #TODO: support migration from deprecated hashes + if isinstance(password, unicode): + # NOTE: encoding password to match file, making the assumption + # that server will use same encoding to hash the password. + password = password.encode(self.encoding) + ok, new_hash = self.context.verify_and_update(password, hash) + if ok and new_hash is not None: + # rehash user's password if old hash was deprecated + self._records[user] = new_hash + self._autosave() + return ok + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="check_password") + def verify(self, user, password): + "verify password for user" + return self.check_password(user, password) -#========================================================= -#htdigest editing -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htdigest editing +#============================================================================= class HtdigestFile(_CommonFile): - """class for reading & writing Htdigest files + """class for reading & writing Htdigest files. - :arg path: path to htpasswd file to load from / save to (required) + The class constructor accepts the following arguments: - :param autoload: - if ``True`` (the default), :meth:`load` will be automatically called - by constructor. + :type path: filepath + :param path: + + Specifies path to htdigest file, use to implicitly load from and save to. + + This class has two modes of operation: - Set to ``False`` to disable automatic loading (primarily used when - creating new htdigest file). + 1. It can be "bound" to a local file by passing a ``path`` to the class + constructor. In this case it will load the contents of the file when + created, and the :meth:`load` and :meth:`save` methods will automatically + load from and save to that file if they are called without arguments. + 2. Alternately, it can exist as an independant object, in which case + :meth:`load` and :meth:`save` will require an explicit path to be + provided whenever they are called. As well, ``autosave`` behavior + will not be available. + + This feature is new in Passlib 1.6, and is the default if no + ``path`` value is provided to the constructor. + + This is also exposed as a readonly instance attribute. + + :type default_realm: str + :param default_realm: + + If ``default_realm`` is set, all the :class:`HtdigestFile` + methods that require a realm will use this value if one is not + provided explicitly. If unset, they will raise an error stating + that an explicit realm is required. + + This is also exposed as a writeable instance attribute. + + .. versionadded:: 1.6 + + :type new: bool + :param new: + + Normally, if *path* is specified, :class:`HtdigestFile` will + immediately load the contents of the file. However, when creating + a new htpasswd file, applications can set ``new=True`` so that + the existing file (if any) will not be loaded. + + .. versionadded:: 1.6 + This feature was previously enabled by setting ``autoload=False``. + That alias has been deprecated, and will be removed in Passlib 1.8 + + :type autosave: bool + :param autosave: + + Normally, any changes made to an :class:`HtdigestFile` instance + will not be saved until :meth:`save` is explicitly called. However, + if ``autosave=True`` is specified, any changes made will be + saved to disk immediately (assuming *path* has been set). + + This is also exposed as a writeable instance attribute. + + :type encoding: str :param encoding: - optionally specify encoding used for usernames / realms. - if set to ``None``, - user names & realms must be specified as bytes, - and will be returned as bytes. - - if set to an encoding, - user names & realms must be specified as unicode, - and will be returned as unicode. - when stored, then will use the specified encoding. - - for backwards compatibility with passlib 1.4, - this defaults to ``None`` under Python 2, - and ``utf-8`` under Python 3. - - .. note:: - - this is not the encoding for the entire file, - just for the usernames & realms within the file. - this must be an encoding which is compatible - with 7-bit ascii (which is used by rest of file). + Optionally specify character encoding used to read/write file + and hash passwords. Defaults to ``utf-8``, though ``latin-1`` + is the only other commonly encountered encoding. + + This is also exposed as a readonly instance attribute. + + :param autoload: + Set to ``False`` to prevent the constructor from automatically + loaded the file from disk. + + .. deprecated:: 1.6 + This has been replaced by the *new* keyword. + Instead of setting ``autoload=False``, you should use + ``new=True``. Support for this keyword will be removed + in Passlib 1.8. Loading & Saving ================ .. automethod:: load + .. automethod:: load_if_changed + .. automethod:: load_string .. automethod:: save .. automethod:: to_string @@ -401,130 +775,263 @@ ========== .. automethod:: realms .. automethod:: users - .. automethod:: find - .. automethod:: verify + .. automethod:: check_password(user[, realm], password) + .. automethod:: get_hash Modification ============ - .. automethod:: update + .. automethod:: set_password(user[, realm], password) .. automethod:: delete .. automethod:: delete_realm - .. note:: + Alternate Constructors + ====================== + .. automethod:: from_string - All of the methods in this class enforce some data validation - on the ``user`` and ``realm`` parameters: - they will raise a :exc:`ValueError` if either string - contains one of the forbidden characters ``:\\r\\n\\t\\x00``, - or is longer than 255 characters. + Attributes + ========== + .. attribute:: default_realm - """ - #XXX: don't want password encoding to change if user account encoding does. - # but also *can't* use unicode itself. setting this to utf-8 for now, - # until it causes problems - in which case stopgap of setting this attr - # per-instance can be used. - password_encoding = "utf-8" + The default realm that will be used if one is not provided + to methods that require it. By default this is ``None``, + in which case an explicit realm must be provided for every + method call. Can be written to. - #XXX: provide rename() & rename_realm() ? + .. attribute:: path - def _parse_line(self, line): - user, realm, hash = line.rstrip().split(BCOLON) - return (user, realm), hash + Path to local file that will be used as the default + for all :meth:`load` and :meth:`save` operations. + May be written to, initialized by the *path* constructor keyword. - def _render_line(self, key, hash): - return render_bytes("%s:%s:%s\n", key[0], key[1], hash) + .. attribute:: autosave - #TODO: would frontend to calc_digest be useful? - ##def encrypt(self, password, user, realm): - ## user = self._norm_user(user) - ## realm = self._norm_realm(realm) - ## hash = self._calc_digest(user, realm, password) - ## if self.encoding: - ## #decode hash if in unicode mode - ## hash = hash.decode("ascii") - ## return hash + Writeable flag indicating whether changes will be automatically + written to *path*. - def _calc_digest(self, user, realm, password): - "helper to calculate digest" - if isinstance(password, unicode): - password = password.encode(self.password_encoding) - #NOTE: encode('ascii') is noop under py2, required under py3 - return md5(render_bytes("%s:%s:%s", user, realm, password)).hexdigest().encode("ascii") + Errors + ====== + :raises ValueError: + All of the methods in this class will raise a :exc:`ValueError` if + any user name or realm contains a forbidden character (one of ``:\\r\\n\\t\\x00``), + or is longer than 255 characters. + """ + #=================================================================== + # instance attrs + #=================================================================== + + # NOTE: _records map stores (,) for the key, + # and as the value, all as bytes. + + # NOTE: unlike htpasswd, this class doesn't use a CryptContext, + # as only one hash format is supported: htdigest. + + # optionally specify default realm that will be used if none + # is provided to a method call. otherwise realm is always required. + default_realm = None + + #=================================================================== + # init & serialization + #=================================================================== + def __init__(self, path=None, default_realm=None, **kwds): + self.default_realm = default_realm + super(HtdigestFile, self).__init__(path, **kwds) + + def _parse_record(self, record, lineno): + result = record.rstrip().split(_BCOLON) + if len(result) != 3: + raise ValueError("malformed htdigest file (error reading line %d)" + % lineno) + user, realm, hash = result + return (user, realm), hash + + def _render_record(self, key, hash): + user, realm = key + return render_bytes("%s:%s:%s\n", user, realm, hash) + + def _encode_realm(self, realm): + # override default _encode_realm to fill in default realm field + if realm is None: + realm = self.default_realm + if realm is None: + raise TypeError("you must specify a realm explicitly, " + "or set the default_realm attribute") + return self._encode_field(realm, "realm") + + #=================================================================== + # public methods + #=================================================================== def realms(self): - "return all realms listed in file" - return map(self._decode_ident, - set(key[1] for key in self._entry_order)) - - def users(self, realm): - "return list of all users within specified realm" - realm = self._norm_realm(realm) - return map(self._decode_ident, - (key[0] for key in self._entry_order if key[1] == realm)) + """Return list of all realms in database""" + realms = set(key[1] for key in self._records) + return [self._decode_field(realm) for realm in realms] + def users(self, realm=None): + """Return list of all users in specified realm. + + * uses ``self.default_realm`` if no realm explicitly provided. + * returns empty list if realm not found. + """ + realm = self._encode_realm(realm) + return [self._decode_field(key[0]) for key in self._records + if key[1] == realm] + + ##def has_user(self, user, realm=None): + ## "check if user+realm combination exists" + ## user = self._encode_user(user) + ## realm = self._encode_realm(realm) + ## return (user,realm) in self._records + + ##def rename_realm(self, old, new): + ## """rename all accounts in realm""" + ## old = self._encode_realm(old) + ## new = self._encode_realm(new) + ## keys = [key for key in self._records if key[1] == old] + ## for key in keys: + ## hash = self._records.pop(key) + ## self._records[key[0],new] = hash + ## self._autosave() + ## return len(keys) + + ##def rename(self, old, new, realm=None): + ## """rename user account""" + ## old = self._encode_user(old) + ## new = self._encode_user(new) + ## realm = self._encode_realm(realm) + ## hash = self._records.pop((old,realm)) + ## self._records[new,realm] = hash + ## self._autosave() + + def set_password(self, user, realm=None, password=_UNSET): + """Set password for user; adds user & realm if needed. + + If ``self.default_realm`` has been set, this may be called + with the syntax ``set_password(user, password)``, + otherwise it must be called with all three arguments: + ``set_password(user, realm, password)``. + + :returns: + * ``True`` if existing user was updated + * ``False`` if user account added. + """ + if password is _UNSET: + # called w/ two args - (user, password), use default realm + realm, password = None, realm + user = self._encode_user(user) + realm = self._encode_realm(realm) + key = (user, realm) + existing = (key in self._records) + hash = htdigest.encrypt(password, user, realm, encoding=self.encoding) + if PY3: + hash = hash.encode(self.encoding) + self._records[key] = hash + self._autosave() + return existing + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="set_password") def update(self, user, realm, password): - """update password for user under specified realm; adding user if needed + "set password for user" + return self.set_password(user, realm, password) - :returns: ``True`` if existing user was updated, ``False`` if user added. + # XXX: rename to something more explicit, like get_hash()? + def get_hash(self, user, realm=None): + """Return :class:`~passlib.hash.htdigest` hash stored for user. + + * uses ``self.default_realm`` if no realm explicitly provided. + * returns ``None`` if user or realm not found. + + .. versionchanged:: 1.6 + This method was previously named ``find``, it was renamed + for clarity. The old name is deprecated, and will be removed + in Passlib 1.8. """ - user = self._norm_user(user) - realm = self._norm_realm(realm) - key = (user,realm) - hash = self._calc_digest(user, realm, password) - return self._update_key(key, hash) + key = (self._encode_user(user), self._encode_realm(realm)) + hash = self._records.get(key) + if hash is None: + return None + if PY3: + hash = hash.decode(self.encoding) + return hash + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="get_hash") + def find(self, user, realm): + "return hash for user" + return self.get_hash(user, realm) - def delete(self, user, realm): - """delete user's entry for specified realm. + # XXX: rename to something more explicit, like delete_user()? + def delete(self, user, realm=None): + """Delete user's entry for specified realm. - :returns: ``True`` if user deleted, ``False`` if user not found in realm. + if realm is not specified, uses ``self.default_realm``. + + :returns: + * ``True`` if user deleted, + * ``False`` if user not found in realm. """ - user = self._norm_user(user) - realm = self._norm_realm(realm) - return self._delete_key((user,realm)) + key = (self._encode_user(user), self._encode_realm(realm)) + try: + del self._records[key] + except KeyError: + return False + self._autosave() + return True def delete_realm(self, realm): - """delete all users for specified realm + """Delete all users for specified realm. - :returns: number of users deleted + if realm is not specified, uses ``self.default_realm``. + + :returns: number of users deleted (0 if realm not found) """ - realm = self._norm_realm(realm) - keys = [ - key for key in self._entry_map - if key[1] == realm - ] + realm = self._encode_realm(realm) + records = self._records + keys = [key for key in records if key[1] == realm] for key in keys: - self._delete_key(key) + del records[key] + self._autosave() return len(keys) - def find(self, user, realm): - """return digest hash for specified user+realm; returns ``None`` if not found + def check_password(self, user, realm=None, password=_UNSET): + """Verify password for specified user + realm. - :returns: htdigest hash or None - :rtype: bytes or None - """ - user = self._norm_user(user) - realm = self._norm_realm(realm) - hash = self._entry_map.get((user,realm)) - if hash is not None and self.encoding: - #decode hash if in unicode mode - hash = hash.decode("ascii") - return hash - - def verify(self, user, realm, password): - """verify password for specified user + realm. + If ``self.default_realm`` has been set, this may be called + with the syntax ``check_password(user, password)``, + otherwise it must be called with all three arguments: + ``check_password(user, realm, password)``. :returns: - * ``None`` if user not found - * ``False`` if password does not match - * ``True`` if password matches. - """ - user = self._norm_user(user) - realm = self._norm_realm(realm) - hash = self._entry_map.get((user,realm)) + * ``None`` if user or realm not found. + * ``False`` if user found, but password does not match. + * ``True`` if user found and password matches. + + .. versionchanged:: 1.6 + This method was previously called ``verify``, it was renamed + to prevent ambiguity with the :class:`!CryptContext` method. + The old alias is deprecated, and will be removed in Passlib 1.8. + """ + if password is _UNSET: + # called w/ two args - (user, password), use default realm + realm, password = None, realm + user = self._encode_user(user) + realm = self._encode_realm(realm) + hash = self._records.get((user,realm)) if hash is None: return None - return hash == self._calc_digest(user, realm, password) + return htdigest.verify(password, hash, user, realm, + encoding=self.encoding) + + @deprecated_method(deprecated="1.6", removed="1.8", + replacement="check_password") + def verify(self, user, realm, password): + "verify password for user" + return self.check_password(user, realm, password) + + #=================================================================== + # eoc + #=================================================================== -#========================================================= +#============================================================================= # eof -#========================================================= +#============================================================================= diff -Nru passlib-1.5.3/passlib/apps.py passlib-1.6.1/passlib/apps.py --- passlib-1.5.3/passlib/apps.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/apps.py 2012-08-01 17:05:50.000000000 +0000 @@ -1,17 +1,16 @@ """passlib.apps""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core import logging; log = logging.getLogger(__name__) from itertools import chain -#site -#libs +# site +# pkg from passlib import hash from passlib.context import LazyCryptContext from passlib.utils import sys_bits -#pkg -#local +# local __all__ = [ 'custom_app_context', 'django_context', @@ -22,49 +21,104 @@ 'postgres_context', ] -#========================================================= -#for quickly bootstrapping new custom applications -#========================================================= +#============================================================================= +# master containing all identifiable hashes +#============================================================================= +def _load_master_config(): + from passlib.registry import list_crypt_handlers + + # get master list + schemes = list_crypt_handlers() + + # exclude the ones we know have ambiguous or greedy identify() methods. + excluded = [ + # frequently confused for eachother + 'bigcrypt', + 'crypt16', + + # no good identifiers + 'cisco_pix', + 'cisco_type7', + 'htdigest', + 'mysql323', + 'oracle10', + + # all have same size + 'lmhash', + 'msdcc', + 'msdcc2', + 'nthash', + + # plaintext handlers + 'plaintext', + 'ldap_plaintext', + + # disabled handlers + 'django_disabled', + 'unix_disabled', + 'unix_fallback', + ] + for name in excluded: + schemes.remove(name) + + # return config + return dict(schemes=schemes, default="sha256_crypt") +master_context = LazyCryptContext(onload=_load_master_config) + +#============================================================================= +# for quickly bootstrapping new custom applications +#============================================================================= custom_app_context = LazyCryptContext( - #choose some reasonbly strong schemes + # choose some reasonbly strong schemes schemes=["sha512_crypt", "sha256_crypt"], - #set some useful global options - all__vary_rounds = "10%", + # set some useful global options default="sha256_crypt" if sys_bits < 64 else "sha512_crypt", + all__vary_rounds = 0.1, - #set a good starting point for rounds selection - sha512_crypt__default_rounds = 40000, - sha256_crypt__default_rounds = 40000, - - #if the admin user category is selected, make a much stronger hash, - admin__sha512_crypt__default_rounds = 80000, - admin__sha256_crypt__default_rounds = 80000, + # set a good starting point for rounds selection + sha512_crypt__min_rounds = 60000, + sha256_crypt__min_rounds = 80000, + + # if the admin user category is selected, make a much stronger hash, + admin__sha512_crypt__min_rounds = 120000, + admin__sha256_crypt__min_rounds = 160000, ) -#========================================================= -#django -#========================================================= -django_context = LazyCryptContext( - schemes=[ +#============================================================================= +# django +#============================================================================= +_django10_schemes = [ "django_salted_sha1", "django_salted_md5", "django_des_crypt", "hex_md5", "django_disabled", - ], +] + +django10_context = LazyCryptContext( + schemes=_django10_schemes, default="django_salted_sha1", deprecated=["hex_md5"], ) -#========================================================= -#ldap -#========================================================= +django14_context = LazyCryptContext( + schemes=["django_pbkdf2_sha256", "django_pbkdf2_sha1", "django_bcrypt"] \ + + _django10_schemes, + deprecated=_django10_schemes, +) + +# this will always point to latest version +django_context = django14_context + +#============================================================================= +# ldap +#============================================================================= std_ldap_schemes = ["ldap_salted_sha1", "ldap_salted_md5", "ldap_sha1", "ldap_md5", "ldap_plaintext" ] -#create context with all std ldap schemes EXCEPT crypt +# create context with all std ldap schemes EXCEPT crypt ldap_nocrypt_context = LazyCryptContext(std_ldap_schemes) -#create context with all possible std ldap + ldap crypt schemes +# create context with all possible std ldap + ldap crypt schemes def _iter_ldap_crypt_schemes(): from passlib.utils import unix_crypt_schemes return ('ldap_' + name for name in unix_crypt_schemes) @@ -74,53 +128,50 @@ return chain(std_ldap_schemes, _iter_ldap_crypt_schemes()) ldap_context = LazyCryptContext(_iter_ldap_schemes()) -###create context with all std ldap schemes + crypt schemes for localhost +### create context with all std ldap schemes + crypt schemes for localhost ##def _iter_host_ldap_schemes(): ## "helper which iterates over supported std ldap schemes" ## from passlib.handlers.ldap_digests import get_host_ldap_crypt_schemes ## return chain(std_ldap_schemes, get_host_ldap_crypt_schemes()) ##ldap_host_context = LazyCryptContext(_iter_host_ldap_schemes()) -#========================================================= -#mysql -#========================================================= +#============================================================================= +# mysql +#============================================================================= mysql3_context = LazyCryptContext(["mysql323"]) mysql4_context = LazyCryptContext(["mysql41", "mysql323"], deprecated="mysql323") -mysql_context = mysql4_context #tracks latest mysql version supported +mysql_context = mysql4_context # tracks latest mysql version supported -#========================================================= -#postgres -#========================================================= +#============================================================================= +# postgres +#============================================================================= postgres_context = LazyCryptContext(["postgres_md5"]) -#========================================================= -#phpass & variants -#========================================================= +#============================================================================= +# phpass & variants +#============================================================================= def _create_phpass_policy(**kwds): - "helper to make bcrypt default ONLY if it's available" - from passlib.context import default_policy - if hash.bcrypt.has_backend(): - kwds['default'] = 'bcrypt' - return default_policy.replace(**kwds) + "helper to choose default alg based on bcrypt availability" + kwds['default'] = 'bcrypt' if hash.bcrypt.has_backend() else 'phpass' + return kwds phpass_context = LazyCryptContext( schemes=["bcrypt", "phpass", "bsdi_crypt"], - default="phpass", #NOTE: <-- overridden by create_policy - create_policy=_create_phpass_policy, + onload=_create_phpass_policy, ) phpbb3_context = LazyCryptContext(["phpass"], phpass__ident="H") -#TODO: support the drupal phpass variants (see phpass homepage) +# TODO: support the drupal phpass variants (see phpass homepage) -#========================================================= -#roundup -#========================================================= +#============================================================================= +# roundup +#============================================================================= _std_roundup_schemes = [ "ldap_hex_sha1", "ldap_hex_md5", "ldap_des_crypt", "roundup_plaintext" ] roundup10_context = LazyCryptContext(_std_roundup_schemes) -#NOTE: 'roundup15' really applies to roundup 1.4.17+ +# NOTE: 'roundup15' really applies to roundup 1.4.17+ roundup_context = roundup15_context = LazyCryptContext( schemes=_std_roundup_schemes + [ "ldap_pbkdf2_sha1" ], deprecated=_std_roundup_schemes, @@ -128,6 +179,6 @@ ldap_pbkdf2_sha1__default_rounds = 10000, ) -#========================================================= +#============================================================================= # eof -#========================================================= +#============================================================================= diff -Nru passlib-1.5.3/passlib/context.py passlib-1.6.1/passlib/context.py --- passlib-1.5.3/passlib/context.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/context.py 2012-08-02 18:04:17.000000000 +0000 @@ -1,168 +1,101 @@ """passlib.context - CryptContext implementation""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -from passlib.utils import py32_lang -#core -from cStringIO import StringIO -# Py2k # - #note: importing ConfigParser to handle passlib 1.4 / earlier files -from ConfigParser import SafeConfigParser,ConfigParser,InterpolationSyntaxError -# Py3k # -#if py32_lang: -# #Py3.2 removed old ConfigParser, put SafeConfigParser in it's place -# from ConfigParser import ConfigParser as SafeConfigParser -#else: -# from ConfigParser import SafeConfigParser -# end Py3k # +# core +from functools import update_wrapper import inspect import re import hashlib -from math import log as logb +from math import log as logb, ceil import logging; log = logging.getLogger(__name__) -import time import os +import re +from time import sleep from warnings import warn -#site -try: - from pkg_resources import resource_string -except ImportError: - #not available eg: under GAE - resource_string = None -#libs -from passlib.registry import get_crypt_handler, _unload_handler_name -from passlib.utils import to_bytes, to_unicode, bytes, Undef, \ - is_crypt_handler, splitcomma, rng -#pkg -#local +# site +# pkg +from passlib.exc import PasslibConfigWarning, ExpectedStringError, ExpectedTypeError +from passlib.registry import get_crypt_handler, _validate_handler_name +from passlib.utils import rng, tick, to_bytes, deprecated_method, \ + to_unicode, splitcomma +from passlib.utils.compat import bytes, iteritems, num_types, \ + PY2, PY3, PY_MIN_32, unicode, SafeConfigParser, \ + NativeStringIO, BytesIO, base_string_types +# local __all__ = [ - 'CryptPolicy', 'CryptContext', + 'LazyCryptContext', + 'CryptPolicy', ] -#========================================================= -#crypt policy -#========================================================= - -#-------------------------------------------------------- -#constants controlling parsing of special kwds -#-------------------------------------------------------- - -#: CryptContext kwds which aren't allowed to have category specifiers -_forbidden_category_context_options = frozenset([ "schemes", ]) - #NOTE: forbidding 'schemes' because it would really complicate the behavior - # of CryptContext.identify & CryptContext.lookup. - # most useful behaviors here can be had by overriding deprecated - # and default, anyways. - -#: hash settings which aren't allowed to be set via policy -_forbidden_hash_options = frozenset([ "salt" ]) - #NOTE: doing this for security purposes, why would you ever want a fixed salt? - -#: CryptContext kwds which should be parsed into comma separated list of strings -_context_comma_options = frozenset([ "schemes", "deprecated" ]) - -#-------------------------------------------------------- -#parsing helpers -#-------------------------------------------------------- -def _parse_policy_key(key): - "helper to normalize & parse policy keys; returns ``(category, name, option)``" - orig = key - if '.' not in key and '__' in key: #lets user specifiy programmatically (since python doesn't allow '.') - key = key.replace("__", ".") - parts = key.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 to parse things this way :| - if name == "context": - if opt in _context_comma_options: - if isinstance(value, str): - return splitcomma(value) - elif opt == "min_verify_time": - return float(value) - return value - else: - #try to coerce everything to int - try: - return int(value) - except ValueError: - return value +#============================================================================= +# support +#============================================================================= + +# private object to detect unset params +_UNSET = object() + +# TODO: merge the following helpers into _CryptConfig + +def _coerce_vary_rounds(value): + "parse vary_rounds string to percent as [0,1) float, or integer" + if value.endswith("%"): + # XXX: deprecate this in favor of raw float? + return float(value.rstrip("%"))*.01 + try: + return int(value) + except ValueError: + return float(value) + +# set of options which aren't allowed to be set via policy +_forbidden_scheme_options = set(["salt"]) + # 'salt' - not allowed since a fixed salt would defeat the purpose. + +# dict containing funcs used to coerce strings to correct type +# for scheme option keys. +_coerce_scheme_options = dict( + min_rounds=int, + max_rounds=int, + default_rounds=int, + vary_rounds=_coerce_vary_rounds, + salt_size=int, +) + +def _is_handler_registered(handler): + """detect if handler is registered or a custom handler""" + return get_crypt_handler(handler.name, None) is handler + +#============================================================================= +# crypt policy +#============================================================================= +_preamble = ("The CryptPolicy class has been deprecated as of " + "Passlib 1.6, and will be removed in Passlib 1.8. ") -def parse_policy_items(source): - "helper to parse CryptPolicy options" - # py2k # - if hasattr(source, "iteritems"): - source = source.iteritems() - # py3k # - #if hasattr(source, "items"): - # source = source.items() - # end py3k # - for key, value in source: - cat, name, opt = _parse_policy_key(key) - if name == "context": - if cat and opt in _forbidden_category_context_options: - raise KeyError("%r context option is not allowed per-category" % (opt,)) - else: - if opt in _forbidden_hash_options: - raise KeyError("%r handler option is not allowed to be set via a policy object" % (opt,)) - value = _parse_policy_value(cat, name, opt, value) - yield cat, name, opt, value - -# Py2k # -def _is_legacy_parse_error(err): - "helper for parsing config files" - #NOTE: passlib 1.4 and earlier used ConfigParser, - # when they should have been using SafeConfigParser - # (which passlib 1.5+ switched to) - # this has no real security effects re: passlib, - # but some 1.4 config files that have "vary_rounds = 10%" - # may throw an error under SafeConfigParser, - # and should read "vary_rounds = 10%%" - # - # passlib 1.6 and on will only use SafeConfigParser, - # but passlib 1.5 tries to detect the above 10% error, - # issue a warning, and retry w/ ConfigParser, - # for backward compat. - # - # this function's purpose is to encapsulate that - # backward-compat behavior. - value = err.args[0] - #'%' must be followed by '%' or '(', found: '%' - if value == "'%' must be followed by '%' or '(', found: '%'": - return True - return False -# end Py2k # - -#-------------------------------------------------------- -#policy class proper -#-------------------------------------------------------- class CryptPolicy(object): - """stores configuration options for a CryptContext object. - - The CryptPolicy class constructor accepts a dictionary - of keywords, which can include all the options - listed in the :ref:`list of crypt context options `. + """ + .. deprecated:: 1.6 + This class has been deprecated, and will be removed in Passlib 1.8. + All of it's functionality has been rolled into :class:`CryptContext`. + + This class previously stored the configuration options for the + CryptContext class. In the interest of interface simplification, + all of this class' functionality has been rolled into the CryptContext + class itself. + The documentation for this class is now focused on documenting how to + migrate to the new api. Additionally, where possible, the deprecation + warnings issued by the CryptPolicy methods will list the replacement call + that should be used. Constructors ============ + CryptPolicy objects can be constructed directly using any of + the keywords accepted by :class:`CryptContext`. Direct uses of the + :class:`!CryptPolicy` constructor should either pass the keywords + directly into the CryptContext constructor, or to :meth:`CryptContext.update` + if the policy object was being used to update an existing context object. + In addition to passing in keywords directly, CryptPolicy objects can be constructed by the following methods: @@ -174,6 +107,9 @@ Introspection ============= + All of the informational methods provided by this class have been deprecated + by identical or similar methods in the :class:`CryptContext` class: + .. automethod:: has_schemes .. automethod:: schemes .. automethod:: iter_handlers @@ -190,998 +126,2480 @@ .. automethod:: to_string .. note:: - Instances of CryptPolicy should be treated as immutable. + CryptPolicy are immutable. Use the :meth:`replace` method to mutate existing instances. - """ - #========================================================= - #class methods - #========================================================= + .. deprecated:: 1.6 + """ + #=================================================================== + # class methods + #=================================================================== @classmethod def from_path(cls, path, section="passlib", encoding="utf-8"): - """create new policy from specified section of an ini file. + """create a CryptPolicy instance from a local file. + + .. deprecated:: 1.6 + + Creating a new CryptContext from a file, which was previously done via + ``CryptContext(policy=CryptPolicy.from_path(path))``, can now be + done via ``CryptContext.from_path(path)``. + See :meth:`CryptContext.from_path` for details. + + Updating an existing CryptContext from a file, which was previously done + ``context.policy = CryptPolicy.from_path(path)``, can now be + done via ``context.load_path(path)``. + See :meth:`CryptContext.load_path` for details. + """ + warn(_preamble + + "Instead of ``CryptPolicy.from_path(path)``, " + "use ``CryptContext.from_path(path)`` " + " or ``context.load_path(path)`` for an existing CryptContext.", + DeprecationWarning, stacklevel=2) + return cls(_internal_context=CryptContext.from_path(path, section, + encoding)) - :arg path: path to ini file - :param section: option name of section to read from. - :arg encoding: optional encoding (defaults to utf-8) + @classmethod + def from_string(cls, source, section="passlib", encoding="utf-8"): + """create a CryptPolicy instance from a string. - :raises EnvironmentError: if the file cannot be read + .. deprecated:: 1.6 - :returns: new CryptPolicy instance. + Creating a new CryptContext from a string, which was previously done via + ``CryptContext(policy=CryptPolicy.from_string(data))``, can now be + done via ``CryptContext.from_string(data)``. + See :meth:`CryptContext.from_string` for details. + + Updating an existing CryptContext from a string, which was previously done + ``context.policy = CryptPolicy.from_string(data)``, can now be + done via ``context.load(data)``. + See :meth:`CryptContext.load` for details. """ - #NOTE: we want config parser object to have native strings as keys. - # so we parse as bytes under py2, and unicode under py3. - # - # encoding issues are handled under py2 via to_bytes(), - # which ensures everything is utf-8 internally. + warn(_preamble + + "Instead of ``CryptPolicy.from_string(source)``, " + "use ``CryptContext.from_string(source)`` or " + "``context.load(source)`` for an existing CryptContext.", + DeprecationWarning, stacklevel=2) + return cls(_internal_context=CryptContext.from_string(source, section, + encoding)) - # Py2k # - if encoding == "utf-8": - #we want utf-8 anyways, so just load file in raw mode. - with open(path, "rb") as stream: - return cls._from_stream(stream, section, path) + @classmethod + def from_source(cls, source, _warn=True): + """create a CryptPolicy instance from some source. + + this method autodetects the source type, and invokes + the appropriate constructor automatically. it attempts + to detect whether the source is a configuration string, a filepath, + a dictionary, or an existing CryptPolicy instance. + + .. deprecated:: 1.6 + + Create a new CryptContext, which could previously be done via + ``CryptContext(policy=CryptPolicy.from_source(source))``, should + now be done using an explicit method: the :class:`CryptContext` + constructor itself, :meth:`CryptContext.from_path`, + or :meth:`CryptContext.from_string`. + + Updating an existing CryptContext, which could previously be done via + ``context.policy = CryptPolicy.from_source(source)``, should + now be done using an explicit method: :meth:`CryptContext.update`, + or :meth:`CryptContext.load`. + """ + if _warn: + warn(_preamble + + "Instead of ``CryptPolicy.from_source()``, " + "use ``CryptContext.from_string(path)`` " + " or ``CryptContext.from_path(source)``, as appropriate.", + DeprecationWarning, stacklevel=2) + if isinstance(source, CryptPolicy): + return source + elif isinstance(source, dict): + return cls(_internal_context=CryptContext(**source)) + elif not isinstance(source, (bytes,unicode)): + raise TypeError("source must be CryptPolicy, dict, config string, " + "or file path: %r" % (type(source),)) + elif any(c in source for c in "\n\r\t") or not source.strip(" \t./\;:"): + return cls(_internal_context=CryptContext.from_string(source)) else: - #kinda hacked - load whole file, transcode, and parse. - with open(path, "rb") as stream: - source = stream.read() - source = source.decode(encoding).encode("utf-8") - return cls._from_stream(StringIO(source), section, path) - # Py3k # - #with open(path, "r", encoding=encoding) as stream: - # return cls._from_stream(stream, section, path) - # end Py3k # + return cls(_internal_context=CryptContext.from_path(source)) @classmethod - def from_string(cls, source, section="passlib", encoding="utf-8"): - """create new policy from specified section of an ini-formatted string. + def from_sources(cls, sources, _warn=True): + """create a CryptPolicy instance by merging multiple sources. - :arg source: bytes/unicode string containing ini-formatted content. - :param section: option name of section to read from. - :arg encoding: optional encoding if source is bytes (defaults to utf-8) - - :returns: new CryptPolicy instance. - """ - #NOTE: we want config parser object to have native strings as keys. - # so we parse as bytes under py2, and unicode under py3. - # to handle encoding issues under py2, we use - # "to_bytes()" to transcode to utf-8 as needed. - - # Py2k # - source = to_bytes(source, "utf-8", source_encoding=encoding, errname="source") - # Py3k # - #source = to_unicode(source, encoding, errname="source") - # end Py3k # - return cls._from_stream(StringIO(source), section, "") + each source is interpreted as by :meth:`from_source`, + and the results are merged together. - @classmethod - def _from_stream(cls, stream, section, filename=None): - "helper for from_string / from_path" - # Py2k # - pos = stream.tell() - # end Py2k # + .. deprecated:: 1.6 + Instead of using this method to merge multiple policies together, + a :class:`CryptContext` instance should be created, and then + the multiple sources merged together via :meth:`CryptContext.load`. + """ + if _warn: + warn(_preamble + + "Instead of ``CryptPolicy.from_sources()``, " + "use the various CryptContext constructors " + " followed by ``context.update()``.", + DeprecationWarning, stacklevel=2) + if len(sources) == 0: + raise ValueError("no sources specified") + if len(sources) == 1: + return cls.from_source(sources[0], _warn=False) + kwds = {} + for source in sources: + kwds.update(cls.from_source(source, _warn=False)._context.to_dict(resolve=True)) + return cls(_internal_context=CryptContext(**kwds)) - p = SafeConfigParser() - if py32_lang: - # Py3.2 deprecated readfp - p.read_file(stream, filename or "") + def replace(self, *args, **kwds): + """create a new CryptPolicy, optionally updating parts of the + existing configuration. + + .. deprecated:: 1.6 + Callers of this method should :meth:`CryptContext.update` or + :meth:`CryptContext.copy` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.replace()``, " + "use ``context.update()`` or ``context.copy()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().replace()``, " + "create a CryptContext instance and " + "use ``context.update()`` or ``context.copy()``.", + DeprecationWarning, stacklevel=2) + sources = [ self ] + if args: + sources.extend(args) + if kwds: + sources.append(kwds) + return CryptPolicy.from_sources(sources, _warn=False) + + #=================================================================== + # instance attrs + #=================================================================== + + # internal CryptContext we're wrapping to handle everything + # until this class is removed. + _context = None + + # flag indicating this is wrapper generated by the CryptContext.policy + # attribute, rather than one created independantly by the application. + _stub_policy = False + + #=================================================================== + # init + #=================================================================== + def __init__(self, *args, **kwds): + context = kwds.pop("_internal_context", None) + if context: + assert isinstance(context, CryptContext) + self._context = context + self._stub_policy = kwds.pop("_stub_policy", False) + assert not (args or kwds), "unexpected args: %r %r" % (args,kwds) + else: + if args: + if len(args) != 1: + raise TypeError("only one positional argument accepted") + if kwds: + raise TypeError("cannot specify positional arg and kwds") + kwds = args[0] + warn(_preamble + + "Instead of constructing a CryptPolicy instance, " + "create a CryptContext directly, or use ``context.update()`` " + "and ``context.load()`` to reconfigure existing CryptContext " + "instances.", + DeprecationWarning, stacklevel=2) + self._context = CryptContext(**kwds) + + #=================================================================== + # public interface for examining options + #=================================================================== + def has_schemes(self): + """return True if policy defines *any* schemes for use. + + .. deprecated:: 1.6 + applications should use ``bool(context.schemes())`` instead. + see :meth:`CryptContext.schemes`. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.has_schemes()``, " + "use ``bool(context.schemes())``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().has_schemes()``, " + "create a CryptContext instance and " + "use ``bool(context.schemes())``.", + DeprecationWarning, stacklevel=2) + return bool(self._context.schemes()) + + def iter_handlers(self): + """return iterator over handlers defined in policy. + + .. deprecated:: 1.6 + applications should use ``context.schemes(resolve=True))`` instead. + see :meth:`CryptContext.schemes`. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.iter_handlers()``, " + "use ``context.schemes(resolve=True)``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().iter_handlers()``, " + "create a CryptContext instance and " + "use ``context.schemes(resolve=True)``.", + DeprecationWarning, stacklevel=2) + return self._context.schemes(resolve=True) + + def schemes(self, resolve=False): + """return list of schemes defined in policy. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.schemes` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.schemes()``, " + "use ``context.schemes()``.", + DeprecationWarning, stacklevel=2) else: - p.readfp(stream, filename or "") + warn(_preamble + + "Instead of ``CryptPolicy().schemes()``, " + "create a CryptContext instance and " + "use ``context.schemes()``.", + DeprecationWarning, stacklevel=2) + return list(self._context.schemes(resolve=resolve)) + + def get_handler(self, name=None, category=None, required=False): + """return handler as specified by name, or default handler. - # Py2k # + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.handler` instead, + though note that the ``required`` keyword has been removed, + and the new method will always act as if ``required=True``. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.get_handler()``, " + "use ``context.handler()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().get_handler()``, " + "create a CryptContext instance and " + "use ``context.handler()``.", + DeprecationWarning, stacklevel=2) + # CryptContext.handler() doesn't support required=False, + # so wrapping it in try/except try: - items = p.items(section) - except InterpolationSyntaxError, err: - if not _is_legacy_parse_error(err): + return self._context.handler(name, category) + except KeyError: + if required: raise - #support for deprecated 1.4 behavior, will be removed in 1.6 - if filename: - warn("from_path(): the file %r contains an unescaped '%%', this will be fatal in passlib 1.6" % (filename,), stacklevel=3) else: - warn("from_string(): the provided string contains an unescaped '%', this will be fatal in passlib 1.6", stacklevel=3) - p = ConfigParser() - stream.seek(pos) - p.readfp(stream) - items = p.items(section) - - # py3k # - #items = p.items(section) - # end py3k # + return None - return cls(**dict(items)) + def get_min_verify_time(self, category=None): + """get min_verify_time setting for policy. - @classmethod - def from_source(cls, source): - """create new policy from input. + .. deprecated:: 1.6 + min_verify_time will be removed entirely in passlib 1.8 + """ + warn("get_min_verify_time() and min_verify_time option is deprecated, " + "and will be removed in Passlib 1.8", DeprecationWarning, + stacklevel=2) + return self._context._config.get_context_option_with_flag(category, "min_verify_time")[0] or 0 - :arg source: - source may be a dict, CryptPolicy instance, filepath, or raw string. + def get_options(self, name, category=None): + """return dictionary of options specific to a given handler. - the exact type will be autodetected, and the appropriate constructor called. + .. deprecated:: 1.6 + this method has no direct replacement in the 1.6 api, as there + is not a clearly defined use-case. however, examining the output of + :meth:`CryptContext.to_dict` should serve as the closest alternative. + """ + # XXX: might make a public replacement, but need more study of the use cases. + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "``context.policy.get_options()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "``CryptPolicy().get_options()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + if hasattr(name, "name"): + name = name.name + return self._context._config._get_record_options_with_flag(name, category)[0] - :raises TypeError: if source cannot be identified. + def handler_is_deprecated(self, name, category=None): + """check if handler has been deprecated by policy. - :returns: new CryptPolicy instance. + .. deprecated:: 1.6 + this method has no direct replacement in the 1.6 api, as there + is not a clearly defined use-case. however, examining the output of + :meth:`CryptContext.to_dict` should serve as the closest alternative. """ - if isinstance(source, cls): - #NOTE: can just return source unchanged, - #since we're treating CryptPolicy objects as read-only - return source + # XXX: might make a public replacement, but need more study of the use cases. + if self._stub_policy: + warn(_preamble + + "``context.policy.handler_is_deprecated()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "``CryptPolicy().handler_is_deprecated()`` will no longer be available.", + DeprecationWarning, stacklevel=2) + if hasattr(name, "name"): + name = name.name + return self._context._is_deprecated_scheme(name, category) - elif isinstance(source, dict): - return cls(**source) + #=================================================================== + # serialization + #=================================================================== - elif isinstance(source, (bytes,unicode)): - #FIXME: this autodetection makes me uncomfortable... - if any(c in source for c in "\n\r\t") or not source.strip(" \t./\;:"): #none of these chars should be in filepaths, but should be in config string - return cls.from_string(source) + def iter_config(self, ini=False, resolve=False): + """iterate over key/value pairs representing the policy object. - else: #other strings should be filepath - return cls.from_path(source) + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_dict` instead. + """ + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.iter_config()``, " + "use ``context.to_dict().items()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().iter_config()``, " + "create a CryptContext instance and " + "use ``context.to_dict().items()``.", + DeprecationWarning, stacklevel=2) + # hacked code that renders keys & values in manner that approximates + # old behavior. context.to_dict() is much cleaner. + context = self._context + if ini: + def render_key(key): + return context._render_config_key(key).replace("__", ".") + def render_value(value): + if isinstance(value, (list,tuple)): + value = ", ".join(value) + return value + resolve = False else: - raise TypeError("source must be CryptPolicy, dict, config string, or file path: %r" % (type(source),)) + render_key = context._render_config_key + render_value = lambda value: value + return ( + (render_key(key), render_value(value)) + for key, value in context._config.iter_config(resolve) + ) - @classmethod - def from_sources(cls, sources): - """create new policy from list of existing policy objects. + def to_dict(self, resolve=False): + """export policy object as dictionary of options. - 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. + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_dict` instead. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.to_dict()``, " + "use ``context.to_dict()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().to_dict()``, " + "create a CryptContext instance and " + "use ``context.to_dict()``.", + DeprecationWarning, stacklevel=2) + return self._context.to_dict(resolve) + + def to_file(self, stream, section="passlib"): # pragma: no cover -- deprecated & unused + """export policy to file. + + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_string` instead, + and then write the output to a file as desired. + """ + if self._stub_policy: + warn(_preamble + + "Instead of ``context.policy.to_file(stream)``, " + "use ``stream.write(context.to_string())``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().to_file(stream)``, " + "create a CryptContext instance and " + "use ``stream.write(context.to_string())``.", + DeprecationWarning, stacklevel=2) + out = self._context.to_string(section=section) + if PY2: + out = out.encode("utf-8") + stream.write(out) - :arg sources: list of sources to build policy from, elements may be any type accepted by :meth:`from_source`. + def to_string(self, section="passlib", encoding=None): + """export policy to file. - :returns: new CryptPolicy instance + .. deprecated:: 1.6 + applications should use :meth:`CryptContext.to_string` instead. """ - #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? - raise ValueError("no sources specified") + if self._stub_policy: + warn(_preamble + # pragma: no cover -- deprecated & unused + "Instead of ``context.policy.to_string()``, " + "use ``context.to_string()``.", + DeprecationWarning, stacklevel=2) + else: + warn(_preamble + + "Instead of ``CryptPolicy().to_string()``, " + "create a CryptContext instance and " + "use ``context.to_string()``.", + DeprecationWarning, stacklevel=2) + out = self._context.to_string(section=section) + if encoding: + out = out.encode(encoding) + return out - #check if only one source - if len(sources) == 1: - return cls.from_source(sources[0]) + #=================================================================== + # eoc + #=================================================================== - #else, build up list of kwds by parsing each source - kwds = {} - for source in sources: - policy = cls.from_source(source) - kwds.update(policy.iter_config(resolve=True)) +#============================================================================= +# _CryptRecord helper class +#============================================================================= +class _CryptRecord(object): + """wraps a handler and automatically applies various options. + + this is a helper used internally by CryptContext in order to reduce the + amount of work that needs to be done by CryptContext.verify(). + this class takes in all the options for a particular (scheme, category) + combination, and attempts to provide as short a code-path as possible for + the particular configuration. + """ - #build new policy - return cls(**kwds) + #=================================================================== + # instance attrs + #=================================================================== - def replace(self, *args, **kwds): - """return copy of policy, with specified options replaced by new values. + # informational attrs + handler = None # handler instance this is wrapping + category = None # user category this applies to + deprecated = False # set if handler itself has been deprecated in config + + # rounds management - filled in by _init_rounds_options() + _has_rounds_options = False # if _has_rounds_bounds OR _generate_rounds is set + _has_rounds_bounds = False # if either min_rounds or max_rounds set + _min_rounds = None # minimum rounds allowed by policy, or None + _max_rounds = None # maximum rounds allowed by policy, or None + _generate_rounds = None # rounds generation function, or None - 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. + # encrypt()/genconfig() attrs + settings = None # options to be passed directly to encrypt() - :param \*args: optional list of sources as accepted by :meth:`from_sources`. - :param \*\*kwds: optional specific options to override in the new policy. + # verify() attrs + _min_verify_time = None - :returns: new CryptPolicy instance - """ - sources = [ self ] - if args: - sources.extend(args) - if kwds: - sources.append(kwds) - return CryptPolicy.from_sources(sources) + # needs_update() attrs + _needs_update = None # optional callable provided by handler + _has_rounds_introspection = False # if rounds can be extract from hash + + # cloned directly from handler, not affected by config options. + identify = None + genhash = None - #========================================================= - #instance attrs - #========================================================= - #NOTE: all category dictionaries below will have a minimum of 'None' as a key + #=================================================================== + # init + #=================================================================== + def __init__(self, handler, category=None, deprecated=False, + min_rounds=None, max_rounds=None, default_rounds=None, + vary_rounds=None, min_verify_time=None, + **settings): + # store basic bits + self.handler = handler + self.category = category + self.deprecated = deprecated + self.settings = settings + + # validate & normalize rounds options + self._init_rounds_options(min_rounds, max_rounds, default_rounds, + vary_rounds) + + # init wrappers for handler methods we modify args to + self._init_encrypt_and_genconfig() + self._init_verify(min_verify_time) + self._init_needs_update() + + # these aren't wrapped by _CryptRecord, copy them directly from handler. + self.identify = handler.identify + self.genhash = handler.genhash - #:list of all handlers, in order they will be checked when identifying (reverse of order specified) - _handlers = None #list of password hash handlers instances. + #=================================================================== + # virtual attrs + #=================================================================== + @property + def scheme(self): + return self.handler.name + + @property + def _errprefix(self): + "string used to identify record in error messages" + handler = self.handler + category = self.category + if category: + return "%s %s config" % (handler.name, category) + else: + return "%s config" % (handler.name,) - #:dict mapping category -> default handler for that category - _default = None + def __repr__(self): # pragma: no cover -- debugging + return "<_CryptRecord 0x%x for %s>" % (id(self), self._errprefix) - #:dict mapping category -> set of handler names which are deprecated for that category - _deprecated = None + #=================================================================== + # rounds generation & limits - used by encrypt & deprecation code + #=================================================================== + def _init_rounds_options(self, mn, mx, df, vr): + "parse options and compile efficient generate_rounds function" + #---------------------------------------------------- + # extract hard limits from handler itself + #---------------------------------------------------- + handler = self.handler + if 'rounds' not in handler.setting_kwds: + # doesn't even support rounds keyword. + return + hmn = getattr(handler, "min_rounds", None) + hmx = getattr(handler, "max_rounds", None) + + def check_against_handler(value, name): + "issue warning if value outside handler limits" + if hmn is not None and value < hmn: + warn("%s: %s value is below handler minimum %d: %d" % + (self._errprefix, name, hmn, value), PasslibConfigWarning) + if hmx is not None and value > hmx: + warn("%s: %s value is above handler maximum %d: %d" % + (self._errprefix, name, hmx, value), PasslibConfigWarning) + + #---------------------------------------------------- + # set policy limits + #---------------------------------------------------- + if mn is not None: + if mn < 0: + raise ValueError("%s: min_rounds must be >= 0" % self._errprefix) + check_against_handler(mn, "min_rounds") + self._min_rounds = mn + self._has_rounds_bounds = True + + if mx is not None: + if mn is not None and mx < mn: + raise ValueError("%s: max_rounds must be " + ">= min_rounds" % self._errprefix) + elif mx < 0: + raise ValueError("%s: max_rounds must be >= 0" % self._errprefix) + check_against_handler(mx, "max_rounds") + self._max_rounds = mx + self._has_rounds_bounds = True + + #---------------------------------------------------- + # validate default_rounds + #---------------------------------------------------- + if df is not None: + if mn is not None and df < mn: + raise ValueError("%s: default_rounds must be " + ">= min_rounds" % self._errprefix) + if mx is not None and df > mx: + raise ValueError("%s: default_rounds must be " + "<= max_rounds" % self._errprefix) + check_against_handler(df, "default_rounds") + elif vr or mx or mn: + # need an explicit default to work with + df = getattr(handler, "default_rounds", None) or mx or mn + assert df is not None, "couldn't find fallback default_rounds" + else: + # no need for rounds generation + self._has_rounds_options = self._has_rounds_bounds + return + + # clip default to handler & policy limits *before* vary rounds + # is calculated, so that proportion vr values are scaled against + # the effective default. + def clip(value): + "clip value to intersection of policy + handler limits" + if mn is not None and value < mn: + value = mn + if hmn is not None and value < hmn: + value = hmn + if mx is not None and value > mx: + value = mx + if hmx is not None and value > hmx: + value = hmx + return value + df = clip(df) - #:dict mapping category -> min verify time - _min_verify_time = None + #---------------------------------------------------- + # validate vary_rounds, + # coerce df/vr to linear scale, + # and setup scale_value() to undo coercion + #---------------------------------------------------- + # NOTE: vr=0 same as if vr not set + if vr: + if vr < 0: + raise ValueError("%s: vary_rounds must be >= 0" % + self._errprefix) + def scale_value(value, upper): + return value + if isinstance(vr, float): + # vr is value from 0..1 expressing fraction of default rounds. + if vr > 1: + # XXX: deprecate 1.0 ? + raise ValueError("%s: vary_rounds must be < 1.0" % + self._errprefix) + # calculate absolute vr value based on df & rounds_cost + cost_scale = getattr(handler, "rounds_cost", "linear") + assert cost_scale in ["log2", "linear"] + if cost_scale == "log2": + # convert df & vr to linear scale for limit calc, + # and redefine scale_value() to convert back to log2. + df = 1< 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 - - #:dict mapping (handler name, category) -> dict derived from options. - # this is used to cache results of the get_option() method - _cache = None - - #========================================================= - #init - #========================================================= - def __init__(self, **kwds): - self._from_dict(kwds) - - #========================================================= - #internal init helpers - #========================================================= - def _from_dict(self, kwds): - "configure policy from constructor keywords" - # - #init cache & options - # - context_options = {} - options = self._options = {None:{"context":context_options}} - self._cache = {} + # hack for bsdi_crypt - want to avoid even-valued rounds + # NOTE: this technically might generate a rounds value 1 larger + # than the requested upper bound - but better to err on side of safety. + if getattr(handler, "_avoid_even_rounds", False): + gen = self._generate_rounds + self._generate_rounds = lambda : gen()|1 + self._has_rounds_options = True + + #=================================================================== + # encrypt() / genconfig() + #=================================================================== + def _init_encrypt_and_genconfig(self): + "initialize genconfig/encrypt wrapper methods" + settings = self.settings + handler = self.handler + + # check no invalid settings are being set + keys = handler.setting_kwds + for key in settings: + if key not in keys: + raise KeyError("keyword not supported by %s handler: %r" % + (handler.name, key)) + + # if _prepare_settings() has nothing to do, bypass our wrappers + # with reference to original methods. + if not (settings or self._has_rounds_options): + self.genconfig = handler.genconfig + self.encrypt = handler.encrypt + + def genconfig(self, **kwds): + "wrapper for handler.genconfig() which adds custom settings/rounds" + self._prepare_settings(kwds) + return self.handler.genconfig(**kwds) + + def encrypt(self, secret, **kwds): + "wrapper for handler.encrypt() which adds custom settings/rounds" + self._prepare_settings(kwds) + return self.handler.encrypt(secret, **kwds) + + def _prepare_settings(self, kwds): + "add default values to settings for encrypt & genconfig" + # load in default values for any settings + if kwds: + for k,v in iteritems(self.settings): + if k not in kwds: + kwds[k] = v + else: + # faster, and the common case + kwds.update(self.settings) + + # handle rounds + if self._has_rounds_options: + rounds = kwds.get("rounds") + if rounds is None: + # fill in default rounds value + gen = self._generate_rounds + if gen: + kwds['rounds'] = gen() + elif self._has_rounds_bounds: + # check bounds for application-provided rounds value. + # XXX: should this raise an error instead of warning ? + # NOTE: stackdepth=4 is so that error matches + # where ctx.encrypt() was called by application code. + mn = self._min_rounds + if mn is not None and rounds < mn: + warn("%s requires rounds >= %d, increasing value from %d" % + (self._errprefix, mn, rounds), PasslibConfigWarning, 4) + rounds = mn + mx = self._max_rounds + if mx and rounds > mx: + warn("%s requires rounds <= %d, decreasing value from %d" % + (self._errprefix, mx, rounds), PasslibConfigWarning, 4) + rounds = mx + kwds['rounds'] = rounds + + #=================================================================== + # verify() + #=================================================================== + # TODO: once min_verify_time is removed, this will just be a clone + # of handler.verify() + + def _init_verify(self, mvt): + "initialize verify() wrapper - implements min_verify_time" + if mvt: + assert isinstance(mvt, (int,float)) and mvt > 0, "CryptPolicy should catch this" + self._min_verify_time = mvt + else: + # no mvt wrapper needed, so just use handler.verify directly + self.verify = self.handler.verify + + def verify(self, secret, hash, **context): + "verify helper - adds min_verify_time delay" + mvt = self._min_verify_time + assert mvt > 0, "wrapper should have been replaced for mvt=0" + start = tick() + if self.handler.verify(secret, hash, **context): + return True + end = tick() + delta = mvt + start - end + if delta > 0: + sleep(delta) + elif delta < 0: + # warn app they exceeded bounds (this might reveal + # relative costs of different hashes if under migration) + warn("CryptContext: verify exceeded min_verify_time: " + "scheme=%r min_verify_time=%r elapsed=%r" % + (self.scheme, mvt, end-start), PasslibConfigWarning) + return False + + #=================================================================== + # needs_update() + #=================================================================== + def _init_needs_update(self): + """initialize state for needs_update()""" + # if handler has been deprecated, replace wrapper and skip other checks + if self.deprecated: + self.needs_update = lambda hash, secret: True + return + + # let handler detect hashes with configurations that don't match + # current settings. currently do this by calling + # ``handler._bind_needs_update(**settings)``, which if defined + # should return None or a callable ``needs_update(hash,secret)->bool``. # - #normalize & sort keywords - # - for cat, name, opt, value in parse_policy_items(kwds): - copts = options.get(cat) - if copts is None: - copts = options[cat] = {} - config = copts.get(name) - if config is None: - copts[name] = {opt:value} + # NOTE: this interface is still private, because it was hacked in + # for the sake of bcrypt & scram, and is subject to change. + handler = self.handler + const = getattr(handler, "_bind_needs_update", None) + if const: + self._needs_update = const(**self.settings) + + # XXX: what about a "min_salt_size" deprecator? + + # set flag if we can extract rounds from hash, allowing + # needs_update() to check for rounds that are outside of + # the configured range. + if self._has_rounds_bounds and hasattr(handler, "from_string"): + self._has_rounds_introspection = True + + def needs_update(self, hash, secret): + # init replaces this method entirely for this case. + ### check if handler has been deprecated + ##if self.deprecated: + ## return True + + # check handler's detector if it provided one. + check = self._needs_update + if check and check(hash, secret): + return True + + # XXX: should we use from_string() call below to check + # for config strings, and flag them as needing update? + # or throw an error? + # or leave that as an explicitly undefined border case, + # to keep the codepath simpler & faster? + + # if we can parse rounds parameter, check if it's w/in bounds. + if self._has_rounds_introspection: + # XXX: this might be a good place to use parsehash() + hash_obj = self.handler.from_string(hash) + try: + rounds = hash_obj.rounds + except AttributeError: # pragma: no cover -- sanity check + # XXX: all builtin hashes should have rounds attr, + # so should a warning be issues here? + pass else: - config[opt] = value + mn = self._min_rounds + if mn is not None and rounds < mn: + return True + mx = self._max_rounds + if mx and rounds > mx: + return True - # - #parse list of schemes, and resolve to handlers. - # - schemes = context_options.get("schemes") or [] - handlers = self._handlers = [] - handler_names = set() - for scheme in schemes: - #resolve & validate handler - if is_crypt_handler(scheme): - handler = scheme + return False + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# _CryptConfig helper class +#============================================================================= +class _CryptConfig(object): + """parses, validates, and stores CryptContext config + + this is a helper used internally by CryptContext to handle + parsing, validation, and serialization of it's config options. + split out from the main class, but not made public since + that just complicates interface too much (c.f. CryptPolicy) + + :arg source: config as dict mapping ``(cat,scheme,option) -> value`` + """ + #=================================================================== + # instance attrs + #=================================================================== + + # triple-nested dict which maps scheme -> category -> key -> value, + # storing all hash-specific options + _scheme_options = None + + # double-nested dict which maps key -> category -> value + # storing all CryptContext options + _context_options = None + + # tuple of handler objects + handlers = None + + # tuple of scheme objects in same order as handlers + schemes = None + + # tuple of categories in alphabetical order (not including None) + categories = None + + # dict mapping category -> default scheme + _default_schemes = None + + # dict mapping (scheme, category) -> _CryptRecord + _records = None + + # dict mapping category -> list of _CryptRecord instances for that category, + # in order of schemes(). populated on demand by _get_record_list() + _record_lists = None + + #=================================================================== + # constructor + #=================================================================== + def __init__(self, source): + self._init_scheme_list(source.get((None,None,"schemes"))) + self._init_options(source) + self._init_default_schemes() + self._init_records() + + def _init_scheme_list(self, data): + """initialize .handlers and .schemes attributes""" + handlers = [] + schemes = [] + if isinstance(data, str): + data = splitcomma(data) + for elem in data or (): + # resolve elem -> handler & scheme + if hasattr(elem, "name"): + handler = elem + scheme = handler.name + _validate_handler_name(scheme) + elif isinstance(elem, str): + handler = get_crypt_handler(elem) + scheme = handler.name else: - handler = get_crypt_handler(scheme) - name = handler.name - if not name: - raise TypeError("handler lacks name: %r" % (handler,)) - - #check name hasn't been re-used - if name in handler_names: - #XXX: should this just be a warning ? - raise KeyError("multiple handlers with same name: %r" % (name,)) + raise TypeError("scheme must be name or CryptHandler, " + "not %r" % type(elem)) + + # check scheme name isn't already in use + if scheme in schemes: + raise KeyError("multiple handlers with same name: %r" % + (scheme,)) - #add to handler list + # add to handler list handlers.append(handler) - handler_names.add(name) + schemes.append(scheme) - # - #build _deprecated & _default maps - # - dmap = self._deprecated = {} - fmap = self._default = {} - mvmap = self._min_verify_time = {} - for cat, config in options.iteritems(): - kwds = config.pop("context", None) - if not kwds: - continue - - #list of deprecated schemes - deps = kwds.get("deprecated") or [] - if deps: - if handlers: - for scheme in deps: - if scheme not in handler_names: - raise KeyError("known scheme in deprecated list: %r" % (scheme,)) - dmap[cat] = frozenset(deps) - - #default scheme - fb = kwds.get("default") - if fb: - if handlers: - if hasattr(fb, "name"): - fb = fb.name - if fb not in handler_names: - raise KeyError("unknown scheme set as default: %r" % (fb,)) - fmap[cat] = self.get_handler(fb, required=True) + self.handlers = tuple(handlers) + self.schemes = tuple(schemes) + + #=================================================================== + # lowlevel options + #=================================================================== + + #--------------------------------------------------------------- + # init lowlevel option storage + #--------------------------------------------------------------- + def _init_options(self, source): + """load config dict into internal representation, + and init .categories attr + """ + # prepare dicts & locals + norm_scheme_option = self._norm_scheme_option + norm_context_option = self._norm_context_option + self._scheme_options = scheme_options = {} + self._context_options = context_options = {} + categories = set() + + # load source config into internal storage + for (cat, scheme, key), value in iteritems(source): + categories.add(cat) + if scheme: + # normalize scheme option + key, value = norm_scheme_option(key, value) + + # store in scheme_options + # map structure: scheme_options[scheme][category][key] = value + try: + category_map = scheme_options[scheme] + except KeyError: + scheme_options[scheme] = {cat: {key: value}} + else: + try: + option_map = category_map[cat] + except KeyError: + category_map[cat] = {key: value} + else: + option_map[key] = value + else: + # normalize context option + if cat and key == "schemes": + raise KeyError("'schemes' context option is not allowed " + "per category") + key, value = norm_context_option(key, value) + + # store in context_options + # map structure: context_options[key][category] = value + try: + category_map = context_options[key] + except KeyError: + context_options[key] = {cat: value} else: - fmap[cat] = fb + category_map[cat] = value - #min verify time - value = kwds.get("min_verify_time") - if value: - mvmap[cat] = value - #XXX: error or warning if unknown key found in kwds? - #NOTE: for dmap/fmap/mvmap - - # if no cat=None value is specified, each has it's own defaults, - # (handlers[0] for fmap, set() for dmap, 0 for mvmap) - # but we don't store those in dict since it would complicate policy merge operation - - #========================================================= - #public interface (used by CryptContext) - #========================================================= - def has_schemes(self): - "check if policy supported *any* schemes; returns True/False" - return len(self._handlers) > 0 + # store list of configured categories + categories.discard(None) + self.categories = tuple(sorted(categories)) + + def _norm_scheme_option(self, key, value): + # check for invalid options + if key == "rounds": + # for now, translating this to 'default_rounds' to be helpful. + # need to pick one of the two names as official, + # and deprecate the other one. + key = "default_rounds" + elif key in _forbidden_scheme_options: + raise KeyError("%r option not allowed in CryptContext " + "configuration" % (key,)) + # coerce strings for certain fields (e.g. min_rounds uses ints) + if isinstance(value, str): + func = _coerce_scheme_options.get(key) + if func: + value = func(value) + return key, value + + def _norm_context_option(self, key, value): + schemes = self.schemes + if key == "default": + if hasattr(value, "name"): + value = value.name + elif not isinstance(value, str): + raise ExpectedTypeError(value, "str", "default") + if schemes and value not in schemes: + raise KeyError("default scheme not found in policy") + elif key == "deprecated": + if isinstance(value, str): + value = splitcomma(value) + elif not isinstance(value, (list,tuple)): + raise ExpectedTypeError(value, "str or seq", "deprecated") + if 'auto' in value: + if len(value) > 1: + raise ValueError("cannot list other schemes if " + "``deprecated=['auto']`` is used") + elif schemes: + # make sure list of deprecated schemes is subset of configured schemes + for scheme in value: + if not isinstance(scheme, str): + raise ExpectedTypeError(value, "str", "deprecated element") + if scheme not in schemes: + raise KeyError("deprecated scheme not found " + "in policy: %r" % (scheme,)) + elif key == "min_verify_time": + warn("'min_verify_time' is deprecated as of Passlib 1.6, will be " + "ignored in 1.7, and removed in 1.8.", DeprecationWarning) + value = float(value) + if value < 0: + raise ValueError("'min_verify_time' must be >= 0") + elif key != "schemes": + raise KeyError("unknown CryptContext keyword: %r" % (key,)) + return key, value + + #--------------------------------------------------------------- + # reading context options + #--------------------------------------------------------------- + def get_context_optionmap(self, key, _default={}): + """return dict mapping category->value for specific context option. + (treat retval as readonly). + """ + return self._context_options.get(key, _default) - def iter_handlers(self): - "iterate through handlers for all schemes in policy" - return iter(self._handlers) + def get_context_option_with_flag(self, category, key): + """return value of specific option, handling category inheritance. + also returns flag indicating whether value is category-specific. + """ + try: + category_map = self._context_options[key] + except KeyError: + return None, False + value = category_map.get(None) + if category: + try: + alt = category_map[category] + except KeyError: + pass + else: + if value is None or alt != value: + return alt, True + return value, False + + #--------------------------------------------------------------- + # reading scheme options + #--------------------------------------------------------------- + def _get_scheme_optionmap(self, scheme, category, default={}): + """return all options for (scheme,category) combination + (treat return as readonly) + """ + try: + return self._scheme_options[scheme][category] + except KeyError: + return default - 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_scheme_options_with_flag(self, scheme, category): + """return composite dict of all options set for scheme. + includes options inherited from 'all' and from default category. + result can be modified. + returns (kwds, has_cat_specific_options) + """ + # start out with copy of global options + get_optionmap = self._get_scheme_optionmap + kwds = get_optionmap("all", None).copy() + has_cat_options = False + + # add in category-specific global options + if category: + defkwds = kwds.copy() # <-- used to detect category-specific options + kwds.update(get_optionmap("all", category)) + + # add in default options for scheme + other = get_optionmap(scheme, None) + kwds.update(other) + + # load category-specific options for scheme + if category: + defkwds.update(other) + kwds.update(get_optionmap(scheme, category)) + + # compare default category options to see if there's anything + # category-specific + if kwds != defkwds: + has_cat_options = True - def get_handler(self, name=None, category=None, required=False): - """given the name of a scheme, return handler which manages it. + return kwds, has_cat_options - :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``. - - if name is not specified, attempts to return default handler. - if returning default, and category is specified, returns category-specific default if set. - - :returns: handler attached to specified name or None - """ - if name: - for handler in self._handlers: - if handler.name == name: - return handler - else: - fmap = self._default - if category in fmap: - return fmap[category] - elif category and None in fmap: - return fmap[None] - else: - handlers = self._handlers - if handlers: - return handlers[0] - raise KeyError("no crypt algorithms supported") - if required: - raise KeyError("no crypt algorithm by that name: %r" % (name,)) - return None + #=================================================================== + # deprecated & default schemes + #=================================================================== + def _init_default_schemes(self): + """initialize maps containing default scheme for each category. - def get_options(self, name, category=None): - """return dict of options for specified scheme + have to do this after _init_options(), since the default scheme + is affected by the list of deprecated schemes. + """ + # init maps & locals + get_optionmap = self.get_context_optionmap + default_map = self._default_schemes = get_optionmap("default").copy() + dep_map = get_optionmap("deprecated") + schemes = self.schemes + if not schemes: + return + + # figure out default scheme + deps = dep_map.get(None) or () + default = default_map.get(None) + if not default: + for scheme in schemes: + if scheme not in deps: + default_map[None] = scheme + break + else: + raise ValueError("must have at least one non-deprecated scheme") + elif default in deps: + raise ValueError("default scheme cannot be deprecated") + + # figure out per-category default schemes, + for cat in self.categories: + cdeps = dep_map.get(cat, deps) + cdefault = default_map.get(cat, default) + if not cdefault: + for scheme in schemes: + if scheme not in cdeps: + default_map[cat] = scheme + break + else: + raise ValueError("must have at least one non-deprecated " + "scheme for %r category" % cat) + elif cdefault in cdeps: + raise ValueError("default scheme for %r category " + "cannot be deprecated" % cat) + + def default_scheme(self, category): + "return default scheme for specific category" + defaults = self._default_schemes + try: + return defaults[category] + except KeyError: + pass + if not self.schemes: + raise KeyError("no hash schemes configured for this " + "CryptContext instance") + return defaults[None] + + def is_deprecated_with_flag(self, scheme, category): + "is scheme deprecated under particular category?" + depmap = self.get_context_optionmap("deprecated") + def test(cat): + source = depmap.get(cat, depmap.get(None)) + if source is None: + return None + elif 'auto' in source: + return scheme != self.default_scheme(cat) + else: + return scheme in source + value = test(None) or False + if category: + alt = test(category) + if alt is not None and value != alt: + return alt, True + return value, False - :arg name: name of scheme, or handler instance itself - :param category: optional user category whose options should be returned + #=================================================================== + # CryptRecord objects + #=================================================================== + def _init_records(self): + # NOTE: this step handles final validation of settings, + # checking for violatiions against handler's internal invariants. + # this is why we create all the records now, + # so CryptContext throws error immediately rather than later. + self._record_lists = {} + records = self._records = {} + get_options = self._get_record_options_with_flag + categories = self.categories + for handler in self.handlers: + scheme = handler.name + kwds, _ = get_options(scheme, None) + records[scheme, None] = _CryptRecord(handler, **kwds) + for cat in categories: + kwds, has_cat_options = get_options(scheme, cat) + if has_cat_options: + records[scheme, cat] = _CryptRecord(handler, cat, **kwds) + # NOTE: if handler has no category-specific opts, get_record() + # will automatically use the default category's record. + # NOTE: default records for specific category stored under the + # key (None,category); these are populated on-demand by get_record(). + + def _get_record_options_with_flag(self, scheme, category): + """return composite dict of options for given scheme + category. + + this is currently a private method, though some variant + of it's output may eventually be made public. + + given a scheme & category, it returns two things: + a set of all the keyword options to pass to the _CryptRecord constructor, + and a bool flag indicating whether any of these options + were specific to the named category. if this flag is false, + the options are identical to the options for the default category. - :returns: dict of options for CryptContext internals which are relevant to this name/category combination. + the options dict includes all the scheme-specific settings, + as well as optional *deprecated* and *min_verify_time* keywords. """ - if hasattr(name, "name"): - name = name.name + # get scheme options + kwds, has_cat_options = self.get_scheme_options_with_flag(scheme, category) - cache = self._cache - key = (name, category) + # throw in deprecated flag + value, not_inherited = self.is_deprecated_with_flag(scheme, category) + if value: + kwds['deprecated'] = True + if not_inherited: + has_cat_options = True + + # add in min_verify_time setting from context + value, not_inherited = self.get_context_option_with_flag(category, "min_verify_time") + if value: + kwds['min_verify_time'] = value + if not_inherited: + has_cat_options = True + + return kwds, has_cat_options + + def get_record(self, scheme, category): + "return record for specific scheme & category (cached)" + # NOTE: this is part of the critical path shared by + # all of CryptContext's PasswordHash methods, + # hence all the caching and error checking. + + # quick lookup in cache try: - return cache[key] + return self._records[scheme, category] except KeyError: pass - #TODO: pre-calculate or at least cache some of this. - options = self._options + # type check + if category is not None and not isinstance(category, str): + if PY2 and isinstance(category, unicode): + # for compatibility with unicode-centric py2 apps + return self.get_record(scheme, category.encode("utf-8")) + raise ExpectedTypeError(category, "str or None", "category") + if scheme is not None and not isinstance(scheme, str): + raise ExpectedTypeError(scheme, "str or None", "scheme") + + # if scheme=None, + # use record for category's default scheme, and cache result. + if not scheme: + default = self.default_scheme(category) + assert default + record = self._records[None, category] = self.get_record(default, + category) + return record + + # if no record for (scheme, category), + # use record for (scheme, None), and cache result. + if category: + try: + cache = self._records + record = cache[scheme, category] = cache[scheme, None] + return record + except KeyError: + pass - #start with default values - kwds = options[None].get("all") - if kwds is None: - kwds = {} - else: - kwds = kwds.copy() - - #mix in category default values - if category and category in options: - tmp = options[category].get("all") - 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) + # scheme not found in configuration for default category + raise KeyError("crypt algorithm not found in policy: %r" % (scheme,)) - cache[key] = kwds - return kwds + def _get_record_list(self, category=None): + """return list of records for category (cached) - def handler_is_deprecated(self, name, category=None): - "check if scheme is marked as deprecated according to this policy; returns True/False" - if hasattr(name, "name"): - name = name.name - dmap = self._deprecated - if category in dmap: - return name in dmap[category] - elif category and None in dmap: - return name in dmap[None] + this is an internal helper used only by identify_record() + """ + # type check of category - handled by _get_record() + # quick lookup in cache + try: + return self._record_lists[category] + except KeyError: + pass + # cache miss - build list from scratch + value = self._record_lists[category] = [ + self.get_record(scheme, category) + for scheme in self.schemes + ] + return value + + def identify_record(self, hash, category, required=True): + """internal helper to identify appropriate _CryptRecord for hash""" + # NOTE: this is part of the critical path shared by + # all of CryptContext's PasswordHash methods, + # hence all the caching and error checking. + # FIXME: if multiple hashes could match (e.g. lmhash vs nthash) + # this will only return first match. might want to do something + # about this in future, but for now only hashes with + # unique identifiers will work properly in a CryptContext. + # XXX: if all handlers have a unique prefix (e.g. all are MCF / LDAP), + # could use dict-lookup to speed up this search. + if not isinstance(hash, base_string_types): + raise ExpectedStringError(hash, "hash") + # type check of category - handled by _get_record_list() + for record in self._get_record_list(category): + if record.identify(hash): + return record + if not required: + return None + elif not self.schemes: + raise KeyError("no crypt algorithms supported") else: - return False + raise ValueError("hash could not be identified") - def get_min_verify_time(self, category=None): - "return minimal time that verify() should take, according to this policy" - mvmap = self._min_verify_time - if category in mvmap: - return mvmap[category] - elif category and None in mvmap: - return mvmap[None] - else: - return 0 - - #========================================================= - #serialization - #========================================================= - def iter_config(self, ini=False, resolve=False): - """iterate through key/value pairs of policy configuration + #=================================================================== + # serialization + #=================================================================== + def iter_config(self, resolve=False): + """regenerate original config. - :param ini: - If ``True``, returns data formatted for insertion - into INI file. Keys use ``.`` separator instead of ``__``; - list of handlers returned as comma-separated strings. + this is an iterator which yields ``(cat,scheme,option),value`` items, + in the order they generally appear inside an INI file. + if interpreted as a dictionary, it should match the original + keywords passed to the CryptContext (aside from any canonization). - :param resolve: - If ``True``, returns handler objects instead of handler - names where appropriate. Ignored if ``ini=True``. + it's mainly used as the internal backend for most of the public + serialization methods. + """ + # grab various bits of data + scheme_options = self._scheme_options + context_options = self._context_options + scheme_keys = sorted(scheme_options) + context_keys = sorted(context_options) + + # write loaded schemes (may differ from 'schemes' local var) + if 'schemes' in context_keys: + context_keys.remove("schemes") + value = self.handlers if resolve else self.schemes + if value: + yield (None, None, "schemes"), list(value) + + # then run through config for each user category + for cat in (None,) + self.categories: + + # write context options + for key in context_keys: + try: + value = context_options[key][cat] + except KeyError: + pass + else: + if isinstance(value, list): + value = list(value) + yield (cat, None, key), value + + # write per-scheme options for all schemes. + for scheme in scheme_keys: + try: + kwds = scheme_options[scheme][cat] + except KeyError: + pass + else: + for key in sorted(kwds): + yield (cat, scheme, key), kwds[key] + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# main CryptContext class +#============================================================================= +class CryptContext(object): + """Helper for encrypting passwords using different algorithms. + + Instances of this class allow applications to choose a specific + set of hash algorithms which they wish to support, set limits and defaults + for the rounds and salt sizes those algorithms should use, flag + which algorithms should be deprecated, and automatically handle + migrating users to stronger hashes when they log in. + + Basic usage:: + + >>> ctx = CryptContext(schemes=[...]) + + See the Passlib online documentation for details and full documentation. + """ + # FIXME: altering the configuration of this object isn't threadsafe, + # but is generally only done during application init, so not a major + # issue (just yet). + + # XXX: would like some way to restrict the categories that are allowed, + # to restrict what the app OR the config can use. + + #=================================================================== + # instance attrs + #=================================================================== + + # _CryptConfig instance holding current parsed config + _config = None + + # copy of _config methods, stored in CryptContext instance for speed. + _get_record = None + _identify_record = None + + #=================================================================== + # secondary constructors + #=================================================================== + @classmethod + def _norm_source(cls, source): + "internal helper - accepts string, dict, or context" + if isinstance(source, dict): + return cls(**source) + elif isinstance(source, cls): + return source + else: + self = cls() + self.load(source) + return self + + @classmethod + def from_string(cls, source, section="passlib", encoding="utf-8"): + """create new CryptContext instance from an INI-formatted string. + + :type source: unicode or bytes + :arg source: + string containing INI-formatted content. + + :type section: str + :param section: + option name of section to read from, defaults to ``"passlib"``. + + :type encoding: str + :arg encoding: + optional encoding used when source is bytes, defaults to ``"utf-8"``. :returns: - iterator which yeilds (key,value) pairs. + new :class:`CryptContext` instance, configured based on the + parameters in the *source* string. + + Usage example:: + + >>> from passlib.context import CryptContext + >>> context = CryptContext.from_string(''' + ... [passlib] + ... schemes = sha256_crypt, des_crypt + ... sha256_crypt__default_rounds = 30000 + ... ''') + + .. versionadded:: 1.6 + + .. seealso:: :meth:`to_string`, the inverse of this constructor. """ - # - #prepare formatting functions - # - if ini: - fmt1 = "%s.%s.%s" - fmt2 = "%s.%s" - def encode_handler(h): - return h.name - def encode_hlist(hl): - return ", ".join(h.name for h in hl) - def encode_nlist(hl): - return ", ".join(name for name in hl) - else: - fmt1 = "%s__%s__%s" - fmt2 = "%s__%s" - encode_nlist = list - if resolve: - def encode_handler(h): - return h - encode_hlist = list + if not isinstance(source, base_string_types): + raise ExpectedTypeError(source, "unicode or bytes", "source") + self = cls(_autoload=False) + self.load(source, section=section, encoding=encoding) + return self + + @classmethod + def from_path(cls, path, section="passlib", encoding="utf-8"): + """create new CryptContext instance from an INI-formatted file. + + this functions exactly the same as :meth:`from_string`, + except that it loads from a local file. + + :type path: str + :arg path: + path to local file containing INI-formatted config. + + :type section: str + :param section: + option name of section to read from, defaults to ``"passlib"``. + + :type encoding: str + :arg encoding: + encoding used to load file, defaults to ``"utf-8"``. + + :returns: + new CryptContext instance, configured based on the parameters + stored in the file *path*. + + .. versionadded:: 1.6 + + .. seealso:: :meth:`from_string` for an equivalent usage example. + """ + self = cls(_autoload=False) + self.load_path(path, section=section, encoding=encoding) + return self + + def copy(self, **kwds): + """Return copy of existing CryptContext instance. + + This function returns a new CryptContext instance whose configuration + is exactly the same as the original, with the exception that any keywords + passed in will take precedence over the original settings. + As an example:: + + >>> from passlib.context import CryptContext + + >>> # given an existing context... + >>> ctx1 = CryptContext(["sha256_crypt", "md5_crypt"]) + + >>> # copy can be used to make a clone, and update + >>> # some of the settings at the same time... + >>> ctx2 = custom_app_context.copy(default="md5_crypt") + + >>> # and the original will be unaffected by the change + >>> ctx1.default_scheme() + "sha256_crypt" + >>> ctx2.default_scheme() + "md5_crypt" + + .. versionchanged:: 1.6 + This method was previously named :meth:`!replace`. That alias + has been deprecated, and will be removed in Passlib 1.8. + + .. seealso:: :meth:`update` + """ + # XXX: it would be faster to store ref to self._config, + # but don't want to share config objects til sure + # can rely on them being immutable. + other = CryptContext(_autoload=False) + other.load(self) + if kwds: + other.load(kwds, update=True) + return other + + def replace(self, **kwds): + "deprecated alias of :meth:`copy`" + warn("CryptContext().replace() has been deprecated in Passlib 1.6, " + "and will be removed in Passlib 1.8, " + "it has been renamed to CryptContext().copy()", + DeprecationWarning, stacklevel=2) + return self.copy(**kwds) + + #=================================================================== + # init + #=================================================================== + def __init__(self, schemes=None, + # keyword only... + policy=_UNSET, # <-- deprecated + _autoload=True, **kwds): + # XXX: add ability to make flag certain contexts as immutable, + # e.g. the builtin passlib ones? + # XXX: add a name or import path for the contexts, to help out repr? + if schemes is not None: + kwds['schemes'] = schemes + if policy is not _UNSET: + warn("The CryptContext ``policy`` keyword has been deprecated as of Passlib 1.6, " + "and will be removed in Passlib 1.8; please use " + "``CryptContext.from_string()` or " + "``CryptContext.from_path()`` instead.", + DeprecationWarning) + if policy is None: + self.load(kwds) + elif isinstance(policy, CryptPolicy): + self.load(policy._context) + self.update(kwds) else: - def encode_handler(h): - return h.name - def encode_hlist(hl): - return [ h.name for h in hl ] - - def format_key(cat, name, opt): - if cat: - return fmt1 % (cat, name or "context", opt) - if name: - return fmt2 % (name, opt) - return opt + raise TypeError("policy must be a CryptPolicy instance") + elif _autoload: + self.load(kwds) + else: + assert not kwds, "_autoload=False and kwds are mutually exclusive" + # XXX: would this be useful? + ##def __str__(self): + ## if PY3: + ## return self.to_string() + ## else: + ## return self.to_string().encode("utf-8") + + def __repr__(self): + return "" % id(self) + + #=================================================================== + # deprecated policy object + #=================================================================== + def _get_policy(self): + # The CryptPolicy class has been deprecated, so to support any + # legacy accesses, we create a stub policy object so .policy attr + # will continue to work. # - #run through contents of internal configuration - # - value = self._handlers - if value: - yield format_key(None, None, "schemes"), encode_hlist(value) + # the code waits until app accesses a specific policy object attribute + # before issuing deprecation warning, so developer gets method-specific + # suggestion for how to upgrade. + + # NOTE: making a copy of the context so the policy acts like a snapshot, + # to retain the pre-1.6 behavior. + return CryptPolicy(_internal_context=self.copy(), _stub_policy=True) + + def _set_policy(self, policy): + warn("The CryptPolicy class and the ``context.policy`` attribute have " + "been deprecated as of Passlib 1.6, and will be removed in " + "Passlib 1.8; please use the ``context.load()`` and " + "``context.update()`` methods instead.", + DeprecationWarning, stacklevel=2) + if isinstance(policy, CryptPolicy): + self.load(policy._context) + else: + raise TypeError("expected CryptPolicy instance") - for cat, value in self._deprecated.iteritems(): - yield format_key(cat, None, "deprecated"), encode_nlist(value) + policy = property(_get_policy, _set_policy, + doc="[deprecated] returns CryptPolicy instance " + "tied to this CryptContext") - for cat, value in self._default.iteritems(): - yield format_key(cat, None, "default"), encode_handler(value) + #=================================================================== + # loading / updating configuration + #=================================================================== + @staticmethod + def _parse_ini_stream(stream, section, filename): + "helper read INI from stream, extract passlib section as dict" + # NOTE: this expects a unicode stream under py3, + # and a utf-8 bytes stream under py2, + # allowing the resulting dict to always use native strings. + p = SafeConfigParser() + if PY_MIN_32: + # python 3.2 deprecated readfp in favor of read_file + p.read_file(stream, filename) + else: + p.readfp(stream, filename) + return dict(p.items(section)) - for cat, value in self._min_verify_time.iteritems(): - yield format_key(cat, None, "min_verify_time"), value - - for cat, copts in self._options.iteritems(): - for name in sorted(copts): - config = copts[name] - for opt in sorted(config): - value = config[opt] - yield format_key(cat, name, opt), value + def load_path(self, path, update=False, section="passlib", encoding="utf-8"): + """Load new configuration into CryptContext from a local file. - def to_dict(self, resolve=False): - "return policy as dictionary of keywords" - return dict(self.iter_config(resolve=resolve)) + This function is a wrapper for :meth:`load`, which + loads a configuration string from the local file *path*, + instead of an in-memory source. It's behavior and options + are otherwise identical to :meth:`!load` when provided with + an INI-formatted string. - def _escape_ini_pair(self, k, v): - if isinstance(v, str): - v = v.replace("%", "%%") #escape any percent signs. - elif isinstance(v, (int, long)): - v = str(v) - return k,v + .. versionadded:: 1.6 + """ + def helper(stream): + kwds = self._parse_ini_stream(stream, section, path) + return self.load(kwds, update=update) + if PY3: + # decode to unicode, which load() expected under py3 + with open(path, "rt", encoding=encoding) as stream: + return helper(stream) + elif encoding in ["utf-8", "ascii"]: + # keep as utf-8 bytes, which load() expects under py2 + with open(path, "rb") as stream: + return helper(stream) + else: + # transcode to utf-8 bytes + with open(path, "rb") as fh: + tmp = fh.read().decode(encoding).encode("utf-8") + return helper(BytesIO(tmp)) - def _write_to_parser(self, parser, section): - "helper for to_string / to_file" - parser.add_section(section) - for k,v in self.iter_config(ini=True): - k,v = self._escape_ini_pair(k,v) - parser.set(section, k,v) - - #XXX: rename as "to_stream" or "write_to_stream" ? - def to_file(self, stream, section="passlib"): - "serialize to INI format and write to specified stream" - p = SafeConfigParser() - self._write_to_parser(p, section) - p.write(stream) + def load(self, source, update=False, section="passlib", encoding="utf-8"): + """Load new configuration into CryptContext, replacing existing config. - def to_string(self, section="passlib", encoding=None): - "render to INI string; inverse of from_string() constructor" - buf = StringIO() - self.to_file(buf, section) - out = buf.getvalue() - # Py2k # - out = out.decode("utf-8") - # end Py2k # - if encoding: - out = out.encode(encoding) - return out + :arg source: + source of new configuration to load. + this value can be a number of different types: - ##def to_path(self, path, section="passlib", update=False): - ## "write to INI file" - ## p = ConfigParser() - ## if update and os.path.exists(path): - ## if not p.read([path]): - ## raise EnvironmentError("failed to read existing file") - ## p.remove_section(section) - ## self._write_to_parser(p, section) - ## fh = file(path, "w") - ## p.write(fh) - ## fh.close() + * a :class:`!dict` object, or compatible Mapping - #========================================================= - #eoc - #========================================================= - -#========================================================= -#load default policy from default.cfg -#========================================================= -def _load_default_policy(): - "helper to try to load default policy from file" - #if pkg_resources available, try to read out of egg (common case) - if resource_string: - try: - return CryptPolicy.from_string(resource_string("passlib", "default.cfg")) - except IOError: - log.warn("error reading passlib/default.cfg, is passlib installed correctly?") - pass + the key/value pairs will be interpreted the same + keywords for the :class:`CryptContext` class constructor. - #failing that, see if we can read it from package dir - path = os.path.abspath(os.path.join(os.path.dirname(__file__), "default.cfg")) - if os.path.exists(path): - with open(path, "rb") as fh: - return CryptPolicy.from_string(fh.read()) - - #give up - this is not desirable at all, could use another fallback. - log.error("can't find passlib/default.cfg, is passlib installed correctly?") - return CryptPolicy() - -default_policy = _load_default_policy() - -#========================================================= -# -#========================================================= -class CryptContext(object): - """Helper for encrypting passwords using different algorithms. + * a :class:`!unicode` or :class:`!bytes` string - :param policy: - optionally override the default policy CryptContext starts with before options are added. + this will be interpreted as an INI-formatted file, + and appropriate key/value pairs will be loaded from + the specified *section*. - If not specified, the new instance will inherit a set of default options (such as rounds, etc) - from the passlib default policy (importable as :data:`passlib.context.default_policy`). + * another :class:`!CryptContext` object. - If explicitly set to ``None``, the new instance will not inherit from the default policy, - and will contain only the configuration specified by any additional keywords. + this will export a snapshot of it's configuration + using :meth:`to_dict`. - Alternately, a custom CryptPolicy instance can be passed in, - which allows loading the policy from a configuration file, - combining multiple policies together, and other features. + :type update: bool + :param update: + By default, :meth:`load` will replace the existing configuration + entirely. If ``update=True``, it will preserve any existing + configuration options that are not overridden by the new source, + much like the :meth:`update` method. - :param kwds: + :type section: str + :param section: + When parsing an INI-formatted string, :meth:`load` will look for + a section named ``"passlib"``. This option allows an alternate + section name to be used. Ignored when loading from a dictionary. - ``schemes`` and all other keywords are passed to the CryptPolicy constructor, - or to :meth:`CryptPolicy.replace`, if a policy has also been specified. + :type encoding: str + :param encoding: + Encoding to use when decode bytes from string. + Defaults to ``"utf-8"``. Ignoring when loading from a dictionary. - .. automethod:: replace + :raises TypeError: + * If the source cannot be identified. + * If an unknown / malformed keyword is encountered. - Configuration - ============= - .. attribute:: policy + :raises ValueError: + If an invalid keyword value is encountered. - This exposes the :class:`CryptPolicy` instance - which contains the configuration used by this context object. + .. note:: + + If an error occurs during a :meth:`!load` call, the :class`!CryptContext` + instance will be restored to the configuration it was in before + the :meth:`!load` call was made; this is to ensure it is + *never* left in an inconsistent state due to a load error. + + .. versionadded:: 1.6 + """ + #----------------------------------------------------------- + # autodetect source type, convert to dict + #----------------------------------------------------------- + parse_keys = True + if isinstance(source, base_string_types): + if PY3: + source = to_unicode(source, encoding, param="source") + else: + source = to_bytes(source, "utf-8", source_encoding=encoding, + param="source") + source = self._parse_ini_stream(NativeStringIO(source), section, + "") + elif isinstance(source, CryptContext): + # extract dict directly from config, so it can be merged later + source = dict(source._config.iter_config(resolve=True)) + parse_keys = False + elif not hasattr(source, "items"): + # mappings are left alone, otherwise throw an error. + raise ExpectedTypeError(source, "string or dict", "source") + + # XXX: add support for other iterable types, e.g. sequence of pairs? + + #----------------------------------------------------------- + # parse dict keys into (category, scheme, option) format, + # merge with existing configuration if needed + #----------------------------------------------------------- + if parse_keys: + parse = self._parse_config_key + source = dict((parse(key), value) + for key, value in iteritems(source)) + if update and self._config is not None: + # if updating, do nothing if source is empty, + if not source: + return + # otherwise overlay source on top of existing config + tmp = source + source = dict(self._config.iter_config(resolve=True)) + source.update(tmp) + + #----------------------------------------------------------- + # compile into _CryptConfig instance, and update state + #----------------------------------------------------------- + config = _CryptConfig(source) + self._config = config + self._get_record = config.get_record + self._identify_record = config.identify_record + + @staticmethod + def _parse_config_key(ckey): + """helper used to parse ``cat__scheme__option`` keys into a tuple""" + # split string into 1-3 parts + assert isinstance(ckey, str) + parts = ckey.replace(".","__").split("__") + count = len(parts) + if count == 1: + cat, scheme, key = None, None, parts[0] + elif count == 2: + cat = None + scheme, key = parts + elif count == 3: + cat, scheme, key = parts + else: + raise TypeError("keys must have less than 3 separators: %r" % + (ckey,)) + # validate & normalize the parts + if cat == "default": + cat = None + elif not cat and cat is not None: + raise TypeError("empty category: %r" % ckey) + if scheme == "context": + scheme = None + elif not scheme and scheme is not None: + raise TypeError("empty scheme: %r" % ckey) + if not key: + raise TypeError("empty option: %r" % ckey) + return cat, scheme, key + + def update(self, *args, **kwds): + """Helper for quickly changing configuration. + + This acts much like the :meth:`!dict.update` method: + it updates the context's configuration, + replacing the original value(s) for the specified keys, + and preserving the rest. + It accepts any :ref:`keyword ` + accepted by the :class:`!CryptContext` constructor. + + .. versionadded:: 1.6 + + .. seealso:: :meth:`copy` + """ + if args: + if len(args) > 1: + raise TypeError("expected at most one positional argument") + if kwds: + raise TypeError("positional arg and keywords mutually exclusive") + self.load(args[0], update=True) + elif kwds: + self.load(kwds, update=True) + + # XXX: make this public? even just as flag to load? + # FIXME: this function suffered some bitrot in 1.6.1, + # will need to be updated before works again. + ##def _simplify(self): + ## "helper to remove redundant/unused options" + ## # don't do anything if no schemes are defined + ## if not self._schemes: + ## return + ## + ## def strip_items(target, filter): + ## keys = [key for key,value in iteritems(target) + ## if filter(key,value)] + ## for key in keys: + ## del target[key] + ## + ## # remove redundant default. + ## defaults = self._default_schemes + ## if defaults.get(None) == self._schemes[0]: + ## del defaults[None] + ## + ## # remove options for unused schemes. + ## scheme_options = self._scheme_options + ## schemes = self._schemes + ("all",) + ## strip_items(scheme_options, lambda k,v: k not in schemes) + ## + ## # remove rendundant cat defaults. + ## cur = self.default_scheme() + ## strip_items(defaults, lambda k,v: k and v==cur) + ## + ## # remove redundant category deprecations. + ## # TODO: this should work w/ 'auto', but needs closer inspection + ## deprecated = self._deprecated_schemes + ## cur = self._deprecated_schemes.get(None) + ## strip_items(deprecated, lambda k,v: k and v==cur) + ## + ## # remove redundant category options. + ## for scheme, config in iteritems(scheme_options): + ## if None in config: + ## cur = config[None] + ## strip_items(config, lambda k,v: k and v==cur) + ## + ## # XXX: anything else? - This attribute may be written to (replacing it with another CryptPolicy instance), - in order to reconfigure a CryptContext while an application is running. - However, this should only be done for context instances created by the application, - and NOT for context instances provided by PassLib. - - Main Interface - ============== - .. automethod:: identify - .. automethod:: encrypt - .. automethod:: verify - - Migration Helpers - ================= - .. automethod:: hash_needs_update - .. automethod:: verify_and_update - """ #=================================================================== - #instance attrs + # reading configuration #=================================================================== - policy = None #policy object governing context + def schemes(self, resolve=False): + """return schemes loaded into this CryptContext instance. + + :type resolve: bool + :arg resolve: + if ``True``, will return a tuple of :class:`~passlib.ifc.PasswordHash` + objects instead of their names. + + :returns: + returns tuple of the schemes configured for this context + via the *schemes* option. + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.schemes()`` + + .. seealso:: the :ref:`schemes ` option for usage example. + """ + return self._config.handlers if resolve else self._config.schemes + + # XXX: need to decide if exposing this would be useful to applications + # in any way that isn't already served by to_dict(); + # and then decide whether to expose ability as deprecated_schemes(), + # is_deprecated(), or a just add a schemes(deprecated=True) flag. + def _is_deprecated_scheme(self, scheme, category=None): + "helper used by unittests to check if scheme is deprecated" + return self._get_record(scheme, category).deprecated + + def default_scheme(self, category=None, resolve=False): + """return name of scheme that :meth:`encrypt` will use by default. + + :type resolve: bool + :arg resolve: + if ``True``, will return a :class:`~passlib.ifc.PasswordHash` + object instead of the name. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will return the catgory-specific default scheme instead. + + :returns: + name of the default scheme. + + .. seealso:: the :ref:`default ` option for usage example. + + .. versionadded:: 1.6 + """ + # type check of category - handled by _get_record() + record = self._get_record(None, category) + return record.handler if resolve else record.scheme + + # XXX: need to decide if exposing this would be useful in any way + ##def categories(self): + ## """return user-categories with algorithm-specific options in this CryptContext. + ## + ## this will always return a tuple. + ## if no categories besides the default category have been configured, + ## the tuple will be empty. + ## """ + ## return self._config.categories + + def handler(self, scheme=None, category=None): + """helper to resolve name of scheme -> :class:`~passlib.ifc.PasswordHash` object used by scheme. + + :arg scheme: + This should identify the scheme to lookup. + If omitted or set to ``None``, this will return the handler + for the default scheme. + + :arg category: + If a user category is specified, and no scheme is provided, + it will use the default for that category. + Otherwise this parameter is ignored. + + :raises KeyError: + If the scheme does not exist OR is not being used within this context. + + :returns: + :class:`~passlib.ifc.PasswordHash` object used to implement + the named scheme within this context (this will usually + be one of the objects from :mod:`passlib.hash`) + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.get_handler()`` + """ + try: + return self._get_record(scheme, category).handler + except KeyError: + pass + if self._config.handlers: + raise KeyError("crypt algorithm not found in this " + "CryptContext instance: %r" % (scheme,)) + else: + raise KeyError("no crypt algorithms loaded in this " + "CryptContext instance") + + def _get_unregistered_handlers(self): + "check if any handlers in this context aren't in the global registry" + return tuple(handler for handler in self._config.handlers + if not _is_handler_registered(handler)) #=================================================================== - #init + # exporting config #=================================================================== - def __init__(self, schemes=None, policy=default_policy, **kwds): - #XXX: add a name for the contexts, to help out repr? - if schemes: - kwds['schemes'] = schemes - if not policy: - policy = CryptPolicy(**kwds) - elif kwds: - policy = policy.replace(**kwds) - self.policy = policy + @staticmethod + def _render_config_key(key): + "convert 3-part config key to single string" + cat, scheme, option = key + if cat: + return "%s__%s__%s" % (cat, scheme or "context", option) + elif scheme: + return "%s__%s" % (scheme, option) + else: + return option - def __repr__(self): - #XXX: *could* have proper repr(), but would have to render policy object options, and it'd be *really* long - names = [ handler.name for handler in self.policy.iter_handlers() ] - return "" % (id(self), names) + @staticmethod + def _render_ini_value(key, value): + "render value to string suitable for INI file" + # convert lists to comma separated lists + # (mainly 'schemes' & 'deprecated') + if isinstance(value, (list,tuple)): + value = ", ".join(value) + + # convert numbers to strings + elif isinstance(value, num_types): + if isinstance(value, float) and key[2] == "vary_rounds": + value = ("%.2f" % value).rstrip("0") if value else "0" + else: + value = str(value) - #XXX: make an update() method that just updates policy? + assert isinstance(value, str), \ + "expected string for key: %r %r" % (key, value) - def replace(self, **kwds): - """return mutated CryptContext instance + # escape any percent signs. + return value.replace("%", "%%") - this function operates much like :meth:`datetime.replace()` - it returns - a new CryptContext instance whose configuration is exactly the - same as the original, with the exception of any keywords - specificed taking precedence over the original settings. - - this is identical to the operation ``CryptContext(policy=self.policy.replace(**kwds))``, - see :meth:`CryptPolicy.replace` for more details. - """ - return CryptContext(policy=self.policy.replace(**kwds)) - - #=================================================================== - #policy adaptation - #=================================================================== - def _prepare_rounds(self, handler, opts, settings): - "helper for prepare_default_settings" - 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_rounds") - if vr: - if isinstance(vr, str): - rc = getattr(handler, "rounds_cost", "linear") - vr = int(vr.rstrip("%")) - #NOTE: deliberately strip >1 %, - #in case an interpolation-escaped %% - #makes it through to here. - assert 0 <= vr < 100 - if rc == "log2": - #let % variance scale the number of actual rounds, not the logarithmic value - df = 2**df - vr = int(df*vr/100) - lower = int(logb(df-vr,2)+.5) #err on the side of strength - round up - upper = int(logb(df+vr,2)) - else: - assert rc == "linear" - vr = int(df*vr/100) - lower = df-vr - upper = df+vr - else: - lower = df-vr - upper = df+vr - if lower < 1: - lower = 1 - if mn and lower < mn: - lower = mn - if mx and upper > mx: - upper = mx - if lower > upper: - #NOTE: this mainly happens when default_rounds>max_rounds, which shouldn't usually happen - rounds = upper - warn("vary default rounds: lower bound > upper bound, using upper bound (%d > %d)" % (lower, upper)) - else: - rounds = rng.randint(lower, upper) - 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 - - def _prepare_settings(self, handler, category=None, **settings): - "normalize settings for handler according to context configuration" - opts = self.policy.get_options(handler, category) - if not opts: - return settings - - #load in default values for any settings - for k in handler.setting_kwds: - if k not in settings and k in opts: - settings[k] = opts[k] - - #handle rounds - if 'rounds' in handler.setting_kwds: - self._prepare_rounds(handler, opts, settings) - - #done - 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. - - 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 + def to_dict(self, resolve=False): + """Return current configuration as a dictionary. - :returns: True/False + :type resolve: bool + :arg resolve: + if ``True``, the ``schemes`` key will contain a list of + a :class:`~passlib.ifc.PasswordHash` objects instead of just + their names. + + This method dumps the current configuration of the CryptContext + instance. The key/value pairs should be in the format accepted + by the :class:`!CryptContext` class constructor, in fact + ``CryptContext(**myctx.to_dict())`` will create an exact copy of ``myctx``. + As an example:: + + >>> # you can dump the configuration of any crypt context... + >>> from passlib.apps import ldap_nocrypt_context + >>> ldap_nocrypt_context.to_dict() + {'schemes': ['ldap_salted_sha1', + 'ldap_salted_md5', + 'ldap_sha1', + 'ldap_md5', + 'ldap_plaintext']} + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.to_dict()`` + + .. seealso:: the :ref:`context-serialization-example` example in the tutorial. """ - handler = self.identify(hash, resolve=True, required=True) - policy = self.policy + # XXX: should resolve default to conditional behavior + # based on presence of unregistered handlers? + render_key = self._render_config_key + return dict((render_key(key), value) + for key, value in self._config.iter_config(resolve)) - #check if handler has been deprecated - if policy.handler_is_deprecated(handler, category): - return True + def _write_to_parser(self, parser, section): + "helper to write to ConfigParser instance" + render_key = self._render_config_key + render_value = self._render_ini_value + parser.add_section(section) + for k,v in self._config.iter_config(): + v = render_value(k, v) + k = render_key(k) + parser.set(section, k, v) - #get options, and call compliance helper (check things such as rounds, etc) - opts = policy.get_options(handler, category) + def to_string(self, section="passlib"): + """serialize to INI format and return as unicode string. - #XXX: could check if handler provides it's own helper, eg getattr(handler, "hash_needs_update", None), - #and call that instead of the following default behavior - if hasattr(handler, "_hash_needs_update"): - #NOTE: hacking this in for the sake of bcrypt & issue 25, - # will formalize (and possibly change) interface later. - if handler._hash_needs_update(hash, **opts): - return True - - if opts: - #check if we can parse hash to check it's rounds parameter - if ('min_rounds' in opts or 'max_rounds' in opts) and \ - 'rounds' in handler.setting_kwds and hasattr(handler, "from_string"): - info = handler.from_string(hash) - rounds = getattr(info, "rounds", None) #should generally work, but just in case - if rounds is not None: - min_rounds = opts.get("min_rounds") - if min_rounds is not None and rounds < min_rounds: - return True - max_rounds = opts.get("max_rounds") - if max_rounds is not None and rounds > max_rounds: - return True + :param section: + name of INI section to output, defaults to ``"passlib"``. - return False + :returns: + CryptContext configuration, serialized to a INI unicode string. + + This function acts exactly like :meth:`to_dict`, except that it + serializes all the contents into a single human-readable string, + which can be hand edited, and/or stored in a file. The + output of this method is accepted by :meth:`from_string`, + :meth:`from_path`, and :meth:`load`. As an example:: + + >>> # you can dump the configuration of any crypt context... + >>> from passlib.apps import ldap_nocrypt_context + >>> print ldap_nocrypt_context.to_string() + [passlib] + schemes = ldap_salted_sha1, ldap_salted_md5, ldap_sha1, ldap_md5, ldap_plaintext + + .. versionadded:: 1.6 + This was previously available as ``CryptContext().policy.to_string()`` + + .. seealso:: the :ref:`context-serialization-example` example in the tutorial. + """ + parser = SafeConfigParser() + self._write_to_parser(parser, section) + buf = NativeStringIO() + parser.write(buf) + unregistered = self._get_unregistered_handlers() + if unregistered: + buf.write(( + "# NOTE: the %s handler(s) are not registered with Passlib,\n" + "# this string may not correctly reproduce the current configuration.\n\n" + ) % ", ".join(repr(handler.name) for handler in unregistered)) + out = buf.getvalue() + if not PY3: + out = out.decode("utf-8") + return out + + # XXX: is this useful enough to enable? + ##def write_to_path(self, path, section="passlib", update=False): + ## "write to INI file" + ## parser = ConfigParser() + ## if update and os.path.exists(path): + ## if not parser.read([path]): + ## raise EnvironmentError("failed to read existing file") + ## parser.remove_section(section) + ## self._write_to_parser(parser, section) + ## fh = file(path, "w") + ## parser.write(fh) + ## fh.close() #=================================================================== - #password hash api proxy methods + # password hash api #=================================================================== + + # NOTE: all the following methods do is look up the appropriate + # _CryptRecord for a given (scheme,category) combination, + # and hand off the real work to the record's methods, + # which are optimized for the specific (scheme,category) configuration. + # + # The record objects are cached inside the _CryptConfig + # instance stored in self._config, and are retreived + # via get_record() and identify_record(). + # + # _get_record() and _identify_record() are references + # to _config methods of the same name, + # stored in CryptContext for speed. + + def _get_or_identify_record(self, hash, scheme=None, category=None): + "return record based on scheme, or failing that, by identifying hash" + if scheme: + if not isinstance(hash, base_string_types): + raise ExpectedStringError(hash, "hash") + return self._get_record(scheme, category) + else: + # hash typecheck handled by identify_record() + return self._identify_record(hash, category) + + def needs_update(self, hash, scheme=None, category=None, secret=None): + """Check if hash needs to be replaced for some reason, + in which case the secret should be re-hashed. + + This function is 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. + It returns ``True`` if the hash is using a deprecated scheme, + or is otherwise outside of the bounds specified by the policy + (e.g. the number of rounds is lower than :ref:`min_rounds ` + configuration for that algorithm). + If so, the password should be re-encrypted using :meth:`encrypt` + Otherwise, it will return ``False``. + + :type hash: unicode or bytes + :arg hash: + The hash string to examine. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes ` option). + If no scheme is specified, it will be identified + based on the value of *hash*. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used when determining if the hash needs to be updated + (e.g. is below the minimum rounds). + + :type secret: unicode, bytes, or None + :param secret: + Optionally, the secret associated with the hash. + This is not required, or in fact useful for any current purpose, + and can be safely omitted. It's mainly present to allow the + development of future deprecation checks which might need this information. + + :returns: ``True`` if hash should be replaced, otherwise ``False``. + + .. versionchanged:: 1.6 + The *secret* argument was added, and this method was renamed + from the longer alias ``hash_needs_update``. + + .. seealso:: the :ref:`context-migration-example` example in the tutorial. + """ + record = self._get_or_identify_record(hash, scheme, category) + return record.needs_update(hash, secret) + + @deprecated_method(deprecated="1.6", removed="1.8", replacement="CryptContext.needs_update()") + def hash_needs_update(self, hash, scheme=None, category=None): + """legacy alias for :meth:`needs_update`""" + return self.needs_update(hash, scheme, category) + def genconfig(self, scheme=None, category=None, **settings): - """Call genconfig() for specified handler + """Generate a config string for specified scheme. + + This wraps the :meth:`~passlib.ifc.PasswordHash.genconfig` + method of the appropriate algorithm, using the default if + one is not specified. + The main difference between this and calling a hash's + :meth:`!genconfig` method directly is that this way, the CryptContext + will add in any hash-specific options, such as the default rounds. + + :type scheme: str or None + :param scheme: - 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. + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes ` option). + If no scheme is specified, the configured default + will be used. - 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. + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used when hashing the password (e.g. different default scheme, + different default rounds values, etc). + + :param \*\*settings: + All additional keywords are passed to the appropriate handler, + and should match it's :attr:`~passlib.ifc.PasswordHash.setting_kwds`. + + :returns: + A configuration string suitable for passing to :meth:`~CryptContext.genhash`, + encoding all the provided settings and defaults; or ``None`` + if the selected algorithm doesn't support configuration strings. + The return value will always be a :class:`!str`. """ - handler = self.policy.get_handler(scheme, category, required=True) - settings = self._prepare_settings(handler, category, **settings) - return handler.genconfig(**settings) + return self._get_record(scheme, category).genconfig(**settings) - def genhash(self, secret, config, scheme=None, category=None, **context): - """Call genhash() for specified handler. + def genhash(self, secret, config, scheme=None, category=None, **kwds): + """Generate hash for the specified secret using another hash. - 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. + This wraps the :meth:`~passlib.ifc.PasswordHash.genhash` + method of the appropriate algorithm, identifying it based + on the provided hash / configuration if a scheme is not specified + explicitly. + + :type secret: unicode or bytes + :arg secret: + the password to hash. + + :type config: unicode or bytes + :arg hash: + The hash or configuration string to extract the settings and salt + from when hashing the password. + + :type scheme: str or None + :param scheme: + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes ` option). + If no scheme is specified, it will be identified + based on the value of *config*. + + :type category: str or None + :param category: + Optional :ref:`user category `. + Ignored by this function, this parameter + is provided for symmetry with the other methods. + + :param \*\*kwds: + All additional keywords are passed to the appropriate handler, + and should match it's :attr:`~passlib.ifc.PasswordHash.context_kwds`. + + :returns: + The secret as encoded by the specified algorithm and options. + The return value will always be a :class:`!str`. + + :raises TypeError, ValueError: + * if any of the arguments have an invalid type or value. + * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.genhash` + method throws an error based on *secret* or the provided *kwds*. """ - #NOTE: this doesn't use category in any way, but accepts it for consistency - if scheme: - handler = self.policy.get_handler(scheme, required=True) - else: - handler = self.identify(config, resolve=True, required=True) - #XXX: could insert normalization to preferred unicode encoding here - return handler.genhash(secret, config, **context) + # XXX: could insert normalization to preferred unicode encoding here + return self._get_record(scheme, category).genhash(secret, config, **kwds) def identify(self, hash, category=None, resolve=False, required=False): - """Attempt to identify which algorithm hash belongs to w/in this context. + """Attempt to identify which algorithm the hash belongs to. + + Note that this will only consider the algorithms + currently configured for this context + (see the :ref:`schemes ` option). + All registered algorithms will be checked, from first to last, + and whichever one positively identifies the hash first will be returned. + :type hash: unicode or bytes :arg hash: The hash string to test. + :type category: str or None + :param category: + Optional :ref:`user category `. + Ignored by this function, this parameter + is provided for symmetry with the other methods. + + :type resolve: bool :param resolve: - If ``True``, returns the handler itself, - instead of the name of the handler. + If ``True``, returns the hash handler itself, + instead of the name of the hash. - All registered algorithms will be checked in from last to first, - and whichever one claims the hash first will be returned. + :type required: bool + :param required: + If ``True``, this will raise a ValueError if the hash + cannot be identified, instead of returning ``None``. :returns: 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") + record = self._identify_record(hash, category, required) + if record is None: return None - handler = None - for handler in self.policy.iter_handlers(): - if handler.identify(hash): - if resolve: - return handler - else: - return handler.name - if required: - if handler is None: - raise KeyError("no crypt algorithms supported") - raise ValueError("hash could not be identified") - return None + elif resolve: + return record.handler + else: + return record.scheme def encrypt(self, secret, scheme=None, category=None, **kwds): - """encrypt secret, returning resulting hash. + """run secret through selected algorithm, returning resulting hash. + :type secret: unicode or bytes :arg secret: - String containing the secret to encrypt + the password to hash. + :type scheme: str or None :param scheme: - Optionally specify the name of the algorithm to use. - If no algorithm is specified, an attempt is made - to guess from the hash string. If no hash string - is specified, the last algorithm in the list is used. + + Optional scheme to use. Scheme must be one of the ones + configured for this context (see the + :ref:`schemes ` option). + If no scheme is specified, the configured default + will be used. + + :type category: str or None + :param category: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used when hashing the password (e.g. different default scheme, + different default rounds values, etc). :param \*\*kwds: - All other keyword options are passed to the algorithm's encrypt method. - The two most common ones are "keep_salt" and "rounds". + All other keyword options are passed to the selected algorithm's + :meth:`~passlib.ifc.PasswordHash.encrypt` method. :returns: The secret as encoded by the specified algorithm and options. + The return value will always be a :class:`!str`. + + :raises TypeError, ValueError: + * if any of the arguments have an invalid type or value. + * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.encrypt` + method throws an error based on *secret* or the provided *kwds*. + + .. seealso:: the :ref:`context-basic-example` example in the tutorial """ - handler = self.policy.get_handler(scheme, category, required=True) - kwds = self._prepare_settings(handler, category, **kwds) - #XXX: could insert normalization to preferred unicode encoding here - return handler.encrypt(secret, **kwds) - - def verify(self, secret, hash, scheme=None, category=None, **context): - """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). + # XXX: could insert normalization to preferred unicode encoding here + return self._get_record(scheme, category).encrypt(secret, **kwds) + + def verify(self, secret, hash, scheme=None, category=None, **kwds): + """verify secret against an existing hash. + + If no scheme is specified, this will attempt to identify + the scheme based on the contents of the provided hash + (limited to the schemes configured for this context). + It will then check whether the password verifies against the hash. + :type secret: unicode or bytes :arg secret: the secret to verify + + :type secret: unicode or bytes :arg hash: hash string to compare to - :param scheme: - optional force context to use specfic scheme - (must be listed in context) - :param category: - optional user category, if used by the application. - defaults to ``None``. - :param \*\*context: - all additional keywords are passed to the appropriate handler, - and should match it's - :attr:`context keywords `. - :returns: True/False - """ - #quick checks - if hash is None: - return False - - mvt = self.policy.get_min_verify_time(category) - if mvt: - start = time.time() + :type scheme: str + :param scheme: + Optionally force context to use specific scheme. + This is usually not needed, as most hashes can be unambiguously + identified. Scheme must be one of the ones configured + for this context + (see the :ref:`schemes ` option). - #locate handler - if scheme: - handler = self.policy.get_handler(scheme, required=True) - else: - handler = self.identify(hash, resolve=True, required=True) + :type category: str or None + :param category: + Optional :ref:`user category ` string. + This is mainly used when generating new hashes, it has little + effect when verifying; this keyword is mainly provided for symmetry. - #strip context kwds if scheme doesn't use them - ##for k in context.keys(): - ## if k not in handler.context_kwds: - ## del context[k] + :param \*\*kwds: + All additional keywords are passed to the appropriate handler, + and should match it's :attr:`~passlib.ifc.PasswordHash.context_kwds`. - #XXX: could insert normalization to preferred unicode encoding here + :returns: + ``True`` if the password matched hash, else ``False``. - #use handler to verify secret - result = handler.verify(secret, hash, **context) + :raises TypeError, ValueError: + * if any of the arguments have an invalid type or value. + * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.verify` + method throws an error based on *secret* or the provided *kwds*. - if mvt: - #delta some amount of time if verify took less than mvt seconds - end = time.time() - delta = mvt + start - end - if delta > 0: - time.sleep(delta) - elif delta < 0: - #warn app they aren't being protected against timing attacks... - warn("CryptContext: verify exceeded min_verify_time: scheme=%r min_verify_time=%r elapsed=%r" % - (handler.name, mvt, end-start)) + :raises ValueError: if the hash could not be identified. - return result + .. seealso:: the :ref:`context-basic-example` example in the tutorial + """ + # XXX: have record strip context kwds if scheme doesn't use them? + # XXX: could insert normalization to preferred unicode encoding here + # XXX: what about supporting a setter() callback ala django 1.4 ? + record = self._get_or_identify_record(hash, scheme, category) + return record.verify(secret, hash, **kwds) def verify_and_update(self, secret, hash, scheme=None, category=None, **kwds): - """verify secret and check if hash needs upgrading, in a single call. + """verify password and re-hash the password if needed, all in a single call. - This is a convience method for a common situation in most applications: - When a user logs in, they must :meth:`verify` if the password matches; - if successful, check if the hash algorithm - has been deprecated (:meth:`hash_needs_update`); and if so, - re-:meth:`encrypt` the secret. - This method takes care of calling all of these 3 methods, - returning a simple tuple for the application to use. + This is a convenience method which takes care of all the following: + first it verifies the password (:meth:`~CryptContext.verify`), if this is successfull + it checks if the hash needs updating (:meth:`~CryptContext.needs_update`), and if so, + re-hashes the password (:meth:`~CryptContext.encrypt`), returning the replacement hash. + This series of steps is a very common task for applications + which wish to update deprecated hashes, and this call takes + care of all 3 steps efficiently. + :type secret: unicode or bytes :arg secret: the secret to verify + + :type secret: unicode or bytes :arg hash: hash string to compare to + + :type scheme: str :param scheme: - optional force context to use specfic scheme - (must be listed in context) + Optionally force context to use specific scheme. + This is usually not needed, as most hashes can be unambiguously + identified. Scheme must be one of the ones configured + for this context + (see the :ref:`schemes ` option). + + :type category: str or None :param category: - optional user category, if used by the application. - defaults to ``None``. - :param \*\*context: + Optional :ref:`user category `. + If specified, this will cause any category-specific defaults to + be used if the password has to be re-hashed. + + :param \*\*kwds: all additional keywords are passed to the appropriate handler, - and should match it's - :attr:`context keywords `. + and should match it's :attr:`context keywords `. :returns: - The tuple ``(verified, new_hash)``, where one of the following - cases is true: + This function returns a tuple containing two elements: + the first indicates whether the password verified, + and the second whether the existing hash needs to be replaced. + The return value will always match one of the following 3 cases: * ``(False, None)`` indicates the secret failed to verify. * ``(True, None)`` indicates the secret verified correctly, and the hash does not need upgrading. * ``(True, str)`` indicates the secret verified correctly, - but the existing hash has been deprecated, and should be replaced - by the :class:`str` returned as ``new_hash``. + and the existing hash needs to be updated. the :class:`!str` + will be the freshly generated hash to replace the old one with. - .. seealso:: :ref:`context-migrating-passwords` for a usage example. + .. seealso:: the :ref:`context-migration-example` example in the tutorial. """ - ok = self.verify(secret, hash, scheme=scheme, category=category, **kwds) - if not ok: + # XXX: have record strip context kwds if scheme doesn't use them? + # XXX: could insert normalization to preferred unicode encoding here. + record = self._get_or_identify_record(hash, scheme, category) + if not record.verify(secret, hash, **kwds): return False, None - if self.hash_needs_update(hash, category=category): - return True, self.encrypt(secret, category=category, **kwds) + elif record.needs_update(hash, secret): + # NOTE: we re-encrypt with default scheme, not current one. + return True, self.encrypt(secret, None, category, **kwds) else: return True, None - #========================================================= - #eoc - #========================================================= + #=================================================================== + # eoc + #=================================================================== class LazyCryptContext(CryptContext): """CryptContext subclass which doesn't load handlers until needed. @@ -1192,34 +2610,48 @@ the first time one of it's methods is accessed. :arg schemes: - the first positional argument can be a list of schemes, or omitted, + The first positional argument can be a list of schemes, or omitted, just like CryptContext. - :param create_policy: + :param onload: - if a callable is passed in via this keyword, + If a callable is passed in via this keyword, it will be invoked at lazy-load time with the following signature: - ``create_policy(**kwds) -> CryptPolicy``; + ``onload(**kwds) -> kwds``; where ``kwds`` is all the additional kwds passed to LazyCryptContext. - It should return a CryptPolicy instance, which will then be used - by the CryptContext. + It should perform any additional deferred initialization, + and return the final dict of options to be passed to CryptContext. + + .. versionadded:: 1.6 + + :param create_policy: + + .. deprecated:: 1.6 + This option will be removed in Passlib 1.8, + applications should use ``onload`` instead. :param kwds: - All additional keywords are passed to CryptPolicy; - or to the create_policy function if provided. + All additional keywords are passed to CryptContext; + or to the *onload* function (if provided). This is mainly used internally by modules such as :mod:`passlib.apps`, which define a large number of contexts, but only a few of them will be needed at any one time. Use of this class saves the memory needed to import the specified handlers until the context instance is actually accessed. As well, it allows constructing a context at *module-init* time, - but using :func:`!create_policy()` to provide dynamic configuration + but using :func:`!onload()` to provide dynamic configuration at *application-run* time. """ _lazy_kwds = None + # NOTE: the way this class works changed in 1.6. + # previously it just called _lazy_init() when ``.policy`` was + # first accessed. now that is done whenever any of the public + # attributes are accessed, and the class itself is changed + # to a regular CryptContext, to remove the overhead once it's unneeded. + def __init__(self, schemes=None, **kwds): if schemes is not None: kwds['schemes'] = schemes @@ -1227,26 +2659,29 @@ def _lazy_init(self): kwds = self._lazy_kwds - del self._lazy_kwds if 'create_policy' in kwds: + warn("The CryptPolicy class, and LazyCryptContext's " + "``create_policy`` keyword have been deprecated as of " + "Passlib 1.6, and will be removed in Passlib 1.8; " + "please use the ``onload`` keyword instead.", + DeprecationWarning) create_policy = kwds.pop("create_policy") - kwds = dict(policy=create_policy(**kwds)) + result = create_policy(**kwds) + policy = CryptPolicy.from_source(result, _warn=False) + kwds = policy._context.to_dict() + elif 'onload' in kwds: + onload = kwds.pop("onload") + kwds = onload(**kwds) + del self._lazy_kwds super(LazyCryptContext, self).__init__(**kwds) + self.__class__ = CryptContext - #NOTE: 'policy' property calls _lazy_init the first time it's accessed, - # and relies on CryptContext.__init__ to replace it with an actual instance. - # it should then have no more effect from then on. - class _PolicyProperty(object): - - def __get__(self, obj, cls): - if obj is None: - return self - obj._lazy_init() - assert isinstance(obj.policy, CryptPolicy) - return obj.policy - - policy = _PolicyProperty() + def __getattribute__(self, attr): + if (not attr.startswith("_") or attr.startswith("__")) and \ + self._lazy_kwds is not None: + self._lazy_init() + return object.__getattribute__(self, attr) -#========================================================= +#============================================================================= # eof -#========================================================= +#============================================================================= diff -Nru passlib-1.5.3/passlib/default.cfg passlib-1.6.1/passlib/default.cfg --- passlib-1.5.3/passlib/default.cfg 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/default.cfg 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -[passlib] -# -# this is the PassLib default policy configuration, used by CryptContext -# objects which don't have an explicit base policy specified. -# the goal of this default configuration is not to set any preferred schemes, -# but provide sane defaults (eg rounds) for all the supported algorithms. -# - -#TODO: need to generate min rounds for specific cpu speed & verify time limitations - -all.vary_rounds = 10%% - -bsdi_crypt.default_rounds = 30000 -bcrypt.default_rounds = 10 -sha1_crypt.default_rounds = 30000 -sun_md5_crypt.default_rounds = 30000 -sha256_crypt.default_rounds = 30000 -sha512_crypt.default_rounds = 30000 - -ldap_bsdi_crypt.default_rounds = 30000 -ldap_bcrypt.default_rounds = 10 -ldap_sha1_crypt.default_rounds = 30000 -ldap_sun_md5_crypt.default_rounds = 30000 -ldap_sha256_crypt.default_rounds = 30000 -ldap_sha512_crypt.default_rounds = 30000 - -phpass.default_rounds = 10 diff -Nru passlib-1.5.3/passlib/exc.py passlib-1.6.1/passlib/exc.py --- passlib-1.5.3/passlib/exc.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/exc.py 2012-08-01 20:40:42.000000000 +0000 @@ -0,0 +1,175 @@ +"""passlib.exc -- exceptions & warnings raised by passlib""" +#============================================================================= +# exceptions +#============================================================================= +class MissingBackendError(RuntimeError): + """Error raised if multi-backend handler has no available backends; + or if specifically requested backend is not available. + + :exc:`!MissingBackendError` derives + from :exc:`RuntimeError`, since it usually indicates + lack of an external library or OS feature. + This is primarily raised by handlers which depend on + external libraries (which is currently just + :class:`~passlib.hash.bcrypt`). + """ + +class PasswordSizeError(ValueError): + """Error raised if a password exceeds the maximum size allowed + by Passlib (4096 characters). + + Many password hash algorithms take proportionately larger amounts of time and/or + memory depending on the size of the password provided. This could present + a potential denial of service (DOS) situation if a maliciously large + password is provided to an application. Because of this, Passlib enforces + a maximum size limit, but one which should be *much* larger + than any legitimate password. :exc:`!PasswordSizeError` derives + from :exc:`!ValueError`. + + .. note:: + Applications wishing to use a different limit should set the + ``PASSLIB_MAX_PASSWORD_SIZE`` environmental variable before + Passlib is loaded. The value can be any large positive integer. + + .. versionadded:: 1.6 + """ + def __init__(self): + ValueError.__init__(self, "password exceeds maximum allowed size") + + # this also prevents a glibc crypt segfault issue, detailed here ... + # http://www.openwall.com/lists/oss-security/2011/11/15/1 + +#============================================================================= +# warnings +#============================================================================= +class PasslibWarning(UserWarning): + """base class for Passlib's user warnings. + + .. versionadded:: 1.6 + """ + +class PasslibConfigWarning(PasslibWarning): + """Warning issued when non-fatal issue is found related to the configuration + of a :class:`~passlib.context.CryptContext` instance. + + This occurs primarily in one of two cases: + + * The CryptContext contains rounds limits which exceed the hard limits + imposed by the underlying algorithm. + * An explicit rounds value was provided which exceeds the limits + imposed by the CryptContext. + + In both of these cases, the code will perform correctly & securely; + but the warning is issued as a sign the configuration may need updating. + """ + +class PasslibHashWarning(PasslibWarning): + """Warning issued when non-fatal issue is found with parameters + or hash string passed to a passlib hash class. + + This occurs primarily in one of two cases: + + * A rounds value or other setting was explicitly provided which + exceeded the handler's limits (and has been clamped + by the :ref:`relaxed` flag). + + * A malformed hash string was encountered which (while parsable) + should be re-encoded. + """ + +class PasslibRuntimeWarning(PasslibWarning): + """Warning issued when something unexpected happens during runtime. + + The fact that it's a warning instead of an error means Passlib + was able to correct for the issue, but that it's anonmalous enough + that the developers would love to hear under what conditions it occurred. + """ + +class PasslibSecurityWarning(PasslibWarning): + """Special warning issued when Passlib encounters something + that might affect security. + """ + +#============================================================================= +# error constructors +# +# note: these functions are used by the hashes in Passlib to raise common +# error messages. They are currently just functions which return ValueError, +# rather than subclasses of ValueError, since the specificity isn't needed +# yet; and who wants to import a bunch of error classes when catching +# ValueError will do? +#============================================================================= + +def _get_name(handler): + return handler.name if handler else "" + +#------------------------------------------------------------------------ +# generic helpers +#------------------------------------------------------------------------ +def type_name(value): + "return pretty-printed string containing name of value's type" + cls = value.__class__ + if cls.__module__ and cls.__module__ not in ["__builtin__", "builtins"]: + return "%s.%s" % (cls.__module__, cls.__name__) + elif value is None: + return 'None' + else: + return cls.__name__ + +def ExpectedTypeError(value, expected, param): + "error message when param was supposed to be one type, but found another" + # NOTE: value is never displayed, since it may sometimes be a password. + name = type_name(value) + return TypeError("%s must be %s, not %s" % (param, expected, name)) + +def ExpectedStringError(value, param): + "error message when param was supposed to be unicode or bytes" + return ExpectedTypeError(value, "unicode or bytes", param) + +#------------------------------------------------------------------------ +# encrypt/verify parameter errors +#------------------------------------------------------------------------ +def MissingDigestError(handler=None): + "raised when verify() method gets passed config string instead of hash" + name = _get_name(handler) + return ValueError("expected %s hash, got %s config string instead" % + (name, name)) + +def NullPasswordError(handler=None): + "raised by OS crypt() supporting hashes, which forbid NULLs in password" + name = _get_name(handler) + return ValueError("%s does not allow NULL bytes in password" % name) + +#------------------------------------------------------------------------ +# errors when parsing hashes +#------------------------------------------------------------------------ +def InvalidHashError(handler=None): + "error raised if unrecognized hash provided to handler" + return ValueError("not a valid %s hash" % _get_name(handler)) + +def MalformedHashError(handler=None, reason=None): + "error raised if recognized-but-malformed hash provided to handler" + text = "malformed %s hash" % _get_name(handler) + if reason: + text = "%s (%s)" % (text, reason) + return ValueError(text) + +def ZeroPaddedRoundsError(handler=None): + "error raised if hash was recognized but contained zero-padded rounds field" + return MalformedHashError(handler, "zero-padded rounds") + +#------------------------------------------------------------------------ +# settings / hash component errors +#------------------------------------------------------------------------ +def ChecksumSizeError(handler, raw=False): + "error raised if hash was recognized, but checksum was wrong size" + # TODO: if handler.use_defaults is set, this came from app-provided value, + # not from parsing a hash string, might want different error msg. + checksum_size = handler.checksum_size + unit = "bytes" if raw else "chars" + reason = "checksum must be exactly %d %s" % (checksum_size, unit) + return MalformedHashError(handler, reason) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/ext/django/__init__.py passlib-1.6.1/passlib/ext/django/__init__.py --- passlib-1.5.3/passlib/ext/django/__init__.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/ext/django/__init__.py 2012-05-03 16:36:58.000000000 +0000 @@ -1,10 +1,6 @@ -"""passlib.ext.django - Django app to monkeypatch better password hashing into django +"""passlib.ext.django.models -- monkeypatch django hashing framework -.. warning:: - - This code is experimental and subject to change, - and not officially documented in Passlib just yet - (though it should work). - -see the Passlib documentation for details on how to use this app +this plugin monkeypatches django's hashing framework +so that it uses a passlib context object, allowing handling of arbitrary +hashes in Django databases. """ diff -Nru passlib-1.5.3/passlib/ext/django/models.py passlib-1.6.1/passlib/ext/django/models.py --- passlib-1.5.3/passlib/ext/django/models.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/ext/django/models.py 2012-08-01 20:59:17.000000000 +0000 @@ -1,51 +1,285 @@ -"""passlib.ext.django.models +"""passlib.ext.django.models -- monkeypatch django hashing framework""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +from django import VERSION +from django.conf import settings +# pkg +from passlib.context import CryptContext +from passlib.exc import ExpectedTypeError +from passlib.ext.django.utils import _PatchManager, hasher_to_passlib_name, \ + get_passlib_hasher, get_preset_config +from passlib.utils.compat import callable, unicode, bytes +# local +__all__ = ["password_context"] -.. warning:: +#============================================================================= +# global attrs +#============================================================================= - This code is experimental and subject to change, - and not officially documented in Passlib just yet - (though it should work). - -see the Passlib documentation for details on how to use this app -""" -#=================================================================== -#imports -#=================================================================== -#site -from django.conf import settings -#pkg -from passlib.context import CryptContext, CryptPolicy -from passlib.utils import is_crypt_context, bytes -from passlib.ext.django.utils import DEFAULT_CTX, get_category, \ - set_django_password_context - -#=================================================================== -#main -#=================================================================== -def patch(): - #get config - ctx = getattr(settings, "PASSLIB_CONTEXT", "passlib-default") - catfunc = getattr(settings, "PASSLIB_GET_CATEGORY", get_category) - - #parse & validate input value - if not ctx: - # remove any patching that was already set, just in case. - set_django_password_context(None) +# the context object which this patches contrib.auth to use for password hashing. +# configuration controlled by ``settings.PASSLIB_CONFIG``. +password_context = CryptContext() + +# function mapping User objects -> passlib user category. +# may be overridden via ``settings.PASSLIB_GET_CATEGORY``. +def _get_category(user): + """default get_category() implementation""" + if user.is_superuser: + return "superuser" + elif user.is_staff: + return "staff" + else: + return None + +# object used to track state of patches applied to django. +_manager = _PatchManager(log=logging.getLogger(__name__ + "._manager")) + +# patch status +_patched = False + +#============================================================================= +# applying & removing the patches +#============================================================================= +def _apply_patch(): + """monkeypatch django's password handling to use ``passlib_context``, + assumes the caller will configure the object. + """ + # + # setup constants + # + log.debug("preparing to monkeypatch 'django.contrib.auth' ...") + global _patched + assert not _patched, "monkeypatching already applied" + HASHERS_PATH = "django.contrib.auth.hashers" + MODELS_PATH = "django.contrib.auth.models" + USER_PATH = MODELS_PATH + ":User" + FORMS_PATH = "django.contrib.auth.forms" + + # + # import UNUSUABLE_PASSWORD and is_password_usuable() helpers + # (providing stubs for older django versions) + # + if VERSION < (1,4): + has_hashers = False + if VERSION < (1,0): + UNUSABLE_PASSWORD = "!" + else: + from django.contrib.auth.models import UNUSABLE_PASSWORD + + def is_password_usable(encoded): + return (encoded is not None and encoded != UNUSABLE_PASSWORD) + + def is_valid_secret(secret): + return secret is not None + + else: + has_hashers = True + from django.contrib.auth.hashers import UNUSABLE_PASSWORD, \ + is_password_usable + + def is_valid_secret(secret): + # NOTE: changed in 1.4 - empty passwords no longer valid. + return bool(secret) + + # + # backport ``User.set_unusable_password()`` for Django 0.9 + # (simplifies rest of the code) + # + if not hasattr(_manager.getorig(USER_PATH), "set_unusable_password"): + assert VERSION < (1,0) + + @_manager.monkeypatch(USER_PATH) + def set_unusable_password(user): + user.password = UNUSABLE_PASSWORD + + @_manager.monkeypatch(USER_PATH) + def has_usable_password(user): + return is_password_usable(user.password) + + # + # patch ``User.set_password() & ``User.check_password()`` to use + # context & get_category (would just leave these as wrappers for hashers + # module under django 1.4, but then we couldn't pass User object into + # get_category very easily) + # + @_manager.monkeypatch(USER_PATH) + def set_password(user, password): + "passlib replacement for User.set_password()" + if is_valid_secret(password): + # NOTE: pulls _get_category from module globals + cat = _get_category(user) + user.password = password_context.encrypt(password, category=cat) + else: + user.set_unusable_password() + + @_manager.monkeypatch(USER_PATH) + def check_password(user, password): + "passlib replacement for User.check_password()" + hash = user.password + if not is_valid_secret(password) or not is_password_usable(hash): + return False + # NOTE: pulls _get_category from module globals + cat = _get_category(user) + ok, new_hash = password_context.verify_and_update(password, hash, + category=cat) + if ok and new_hash is not None: + # migrate to new hash if needed. + user.password = new_hash + user.save() + return ok + + # + # override check_password() with our own implementation + # + @_manager.monkeypatch(HASHERS_PATH, enable=has_hashers) + @_manager.monkeypatch(MODELS_PATH) + def check_password(password, encoded, setter=None, preferred="default"): + "passlib replacement for check_password()" + # XXX: this currently ignores "preferred" keyword, since it's purpose + # was for hash migration, and that's handled by the context. + if not is_valid_secret(password) or not is_password_usable(encoded): + return False + ok = password_context.verify(password, encoded) + if ok and setter and password_context.needs_update(encoded): + setter(password) + return ok + + # + # patch the other functions defined in the ``hashers`` module, as well + # as any other known locations where they're imported within ``contrib.auth`` + # + if has_hashers: + @_manager.monkeypatch(HASHERS_PATH) + @_manager.monkeypatch(MODELS_PATH) + def make_password(password, salt=None, hasher="default"): + "passlib replacement for make_password()" + if not is_valid_secret(password): + return UNUSABLE_PASSWORD + kwds = {} + if salt is not None: + kwds['salt'] = salt + if hasher != "default": + kwds['scheme'] = hasher_to_passlib_name(hasher) + return password_context.encrypt(password, **kwds) + + @_manager.monkeypatch(HASHERS_PATH) + @_manager.monkeypatch(FORMS_PATH) + def get_hasher(algorithm="default"): + "passlib replacement for get_hasher()" + if algorithm == "default": + scheme = None + else: + scheme = hasher_to_passlib_name(algorithm) + handler = password_context.handler(scheme) + return get_passlib_hasher(handler) + + # NOTE: custom helper that doesn't exist in django proper + # (though submitted a patch - https://code.djangoproject.com/ticket/18184) + @_manager.monkeypatch(HASHERS_PATH) + @_manager.monkeypatch(FORMS_PATH) + def identify_hasher(encoded): + "passlib helper to identify hasher from encoded password" + handler = password_context.identify(encoded, resolve=True, + required=True) + return get_passlib_hasher(handler) + + _patched = True + log.debug("... finished monkeypatching django") + +def _remove_patch(): + """undo the django monkeypatching done by this module. + offered as a last resort if it's ever needed. + + .. warning:: + This may cause problems if any other Django modules have imported + their own copies of the patched functions, though the patched + code has been designed to throw an error as soon as possible in + this case. + """ + global _patched + if _patched: + log.debug("removing django monkeypatching...") + _manager.unpatch_all(unpatch_conflicts=True) + password_context.load({}) + _patched = False + log.debug("...finished removing django monkeypatching") + return True + if _manager: # pragma: no cover -- sanity check + log.warning("reverting partial monkeypatching of django...") + _manager.unpatch_all() + password_context.load({}) + log.debug("...finished removing django monkeypatching") + return True + log.debug("django not monkeypatched") + return False + +#============================================================================= +# main code +#============================================================================= +def _load(): + global _get_category + + # TODO: would like to add support for inheriting config from a preset + # (or from existing hasher state) and letting PASSLIB_CONFIG + # be an update, not a replacement. + + # TODO: wrap and import any custom hashers as passlib handlers, + # so they could be used in the passlib config. + + # load config from settings + _UNSET = object() + config = getattr(settings, "PASSLIB_CONFIG", _UNSET) + if config is _UNSET: + # XXX: should probably deprecate this alias + config = getattr(settings, "PASSLIB_CONTEXT", _UNSET) + if config is _UNSET: + config = "passlib-default" + if config is None: + warn("setting PASSLIB_CONFIG=None is deprecated, " + "and support will be removed in Passlib 1.8, " + "use PASSLIB_CONFIG='disabled' instead.", + DeprecationWarning) + config = "disabled" + elif not isinstance(config, (unicode, bytes, dict)): + raise ExpectedTypeError(config, "str or dict", "PASSLIB_CONFIG") + + # load custom category func (if any) + get_category = getattr(settings, "PASSLIB_GET_CATEGORY", None) + if get_category and not callable(get_category): + raise ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY") + + # check if we've been disabled + if config == "disabled": + if _patched: # pragma: no cover -- sanity check + log.error("didn't expect monkeypatching would be applied!") + _remove_patch() return - if ctx == "passlib-default": - ctx = DEFAULT_CTX - if isinstance(ctx, (unicode, bytes)): - ctx = CryptPolicy.from_string(ctx) - if isinstance(ctx, CryptPolicy): - ctx = CryptContext(policy=ctx) - if not is_crypt_context(ctx): - raise TypeError("django settings.PASSLIB_CONTEXT must be CryptContext instance or config string: %r" % (ctx,)) - - #monkeypatch django.contrib.auth.models:User - set_django_password_context(ctx, get_category=catfunc) - -patch() - -#=================================================================== -#eof -#=================================================================== + + # resolve any preset aliases + if isinstance(config, str) and '\n' not in config: + config = get_preset_config(config) + + # setup context + _apply_patch() + password_context.load(config) + if get_category: + # NOTE: _get_category is module global which is read by + # monkeypatched functions constructed by _apply_patch() + _get_category = get_category + log.debug("passlib.ext.django loaded") + +# wrap load function so we can undo any patching if something goes wrong +try: + _load() +except: + _remove_patch() + raise + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/ext/django/utils.py passlib-1.6.1/passlib/ext/django/utils.py --- passlib-1.5.3/passlib/ext/django/utils.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/ext/django/utils.py 2012-08-01 17:09:36.000000000 +0000 @@ -1,248 +1,470 @@ -"""passlib.ext.django.utils - helper functions for patching Django hashing - -.. warning:: - - This code is experimental and subject to change, - and not officially documented in Passlib just yet - (though it should work). -""" -#=================================================================== -#imports -#=================================================================== -#site +"""passlib.ext.django.utils - helper functions used by this plugin""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +from weakref import WeakKeyDictionary from warnings import warn -#pkg -from passlib.utils import is_crypt_context, bytes -#local +# site +try: + from django import VERSION as DJANGO_VERSION + log.debug("found django %r installation", DJANGO_VERSION) +except ImportError: + log.debug("django installation not found") + DJANGO_VERSION = () +# pkg +from passlib.context import CryptContext +from passlib.exc import PasslibRuntimeWarning +from passlib.registry import get_crypt_handler, list_crypt_handlers +from passlib.utils import classproperty +from passlib.utils.compat import bytes, get_method_function, iteritems +# local __all__ = [ - "get_category", - "set_django_password_context", + "get_preset_config", + "get_passlib_hasher", ] -#=================================================================== -#lazy imports -#=================================================================== - -_has_django0 = None # old 0.9 django - lacks unusable_password support -_dam = None #django.contrib.auth.models reference - -def _import_django(): - global _dam, _has_django0 - if _dam is None: - import django.contrib.auth.models as _dam - from django import VERSION - _has_django0 = VERSION < (1,0) - return _dam - -#=================================================================== -#constants -#=================================================================== +#============================================================================= +# default policies +#============================================================================= +def get_preset_config(name): + """Returns configuration string for one of the preset strings + supported by the ``PASSLIB_CONFIG`` setting. + Currently supported presets: + + * ``"passlib-default"`` - default config used by this release of passlib. + * ``"django-default"`` - config matching currently installed django version. + * ``"django-latest"`` - config matching newest django version (currently same as ``"django-1.4"``). + * ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs + * ``"django-1.4"`` -config used by stock Django 1.4 installs + """ + # TODO: add preset which includes HASHERS + PREFERRED_HASHERS, + # after having imported any custom hashers. "django-current" + if name == "django-default": + if (0,0) < DJANGO_VERSION < (1,4): + name = "django-1.0" + else: + name = "django-1.4" + if name == "django-1.0": + from passlib.apps import django10_context + return django10_context.to_string() + if name == "django-1.4" or name == "django-latest": + from passlib.apps import django14_context + return django14_context.to_string() + if name == "passlib-default": + return PASSLIB_DEFAULT + raise ValueError("unknown preset config name: %r" % name) -#: base context mirroring django's setup -STOCK_CTX = """ +# default context used by passlib 1.6 +PASSLIB_DEFAULT = """ [passlib] -schemes = - django_salted_sha1, django_salted_md5, - django_des_crypt, hex_md5, - django_disabled - -default = django_salted_sha1 - -deprecated = hex_md5 -""" -#: default context used by app -DEFAULT_CTX = """ -[passlib] +; list of schemes supported by configuration +; currently all django 1.4 hashes, django 1.0 hashes, +; and three common modular crypt format hashes. schemes = - sha512_crypt, - pbkdf2_sha256, - django_salted_sha1, django_salted_md5, - django_des_crypt, hex_md5, - django_disabled + django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, + django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5, + sha512_crypt, bcrypt, phpass -default = sha512_crypt +; default scheme to use for new hashes +default = django_pbkdf2_sha256 +; hashes using these schemes will automatically be re-hashed +; when the user logs in (currently all django 1.0 hashes) deprecated = - pbkdf2_sha256, - django_salted_sha1, django_salted_md5, + django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5 -all__vary_rounds = 5%% - -sha512_crypt__default_rounds = 15000 -staff__sha512_crypt__default_rounds = 25000 -superuser__sha512_crypt__default_rounds = 35000 +; sets some common options, including minimum rounds for two primary hashes. +; if a hash has less than this number of rounds, it will be re-hashed. +all__vary_rounds = 0.05 +sha512_crypt__min_rounds = 80000 +django_pbkdf2_sha256__min_rounds = 10000 + +; set somewhat stronger iteration counts for ``User.is_staff`` +staff__sha512_crypt__default_rounds = 100000 +staff__django_pbkdf2_sha256__default_rounds = 12500 + +; and even stronger ones for ``User.is_superuser`` +superuser__sha512_crypt__default_rounds = 120000 +superuser__django_pbkdf2_sha256__default_rounds = 15000 """ -#=================================================================== -# helpers -#=================================================================== - -def get_category(user): - """default get_category() implementation used by set_django_password_context - - this is the function used if ``settings.PASSLIB_GET_CONTEXT`` is not - specified. - - it maps superusers to the ``"superuser"`` category, - staff to the ``"staff"`` category, - and all others to the default category. - """ - if user.is_superuser: - return "superuser" - if user.is_staff: - return "staff" - return None - -def um(func): - "unwrap method (eg User.set_password -> orig func)" - return func.im_func - -#=================================================================== -# monkeypatch framework -#=================================================================== - -# NOTE: this moneypatcher was written to be useful -# outside of this module, and re-invokable, -# which is why it tries so hard to maintain -# sanity about it's patch state. - -_django_patch_state = None #dict holding refs to undo patch - -def set_django_password_context(context=None, get_category=get_category): - """monkeypatches :mod:`!django.contrib.auth` to use specified password context. - - :arg context: - Passlib context to use for Django password hashing. - If ``None``, restores original Django functions. - - In order to support existing hashes, - any context specified should include - all the hashes in :data:`django_context` - in addition to custom hashes. - - :param get_category: - Optional function to use when mapping Django user -> - CryptContext category. - - If a function, should have syntax ``catfunc(user) -> category|None``. - If ``None``, no function is used. - - By default, uses a function which returns ``"superuser"`` - for superusers, and ``"staff"`` for staff. - - This function monkeypatches the following parts of Django: - - * :func:`!django.contrib.auth.models.check_password` - * :meth:`!django.contrib.auth.models.User.check_password` - * :meth:`!django.contrib.auth.models.User.set_password` - - It also stores the provided context in - :data:`!django.contrib.auth.models.User.password_context`, - for easy access. +#============================================================================= +# translating passlib names <-> hasher names +#============================================================================= + +# prefix used to shoehorn passlib's handler names into django hasher namespace; +# allows get_hasher() to be meaningfully called even if passlib handler +# is the one being used. +PASSLIB_HASHER_PREFIX = "passlib_" + +# prefix all the django-specific hash formats are stored under w/in passlib; +# all of these hashes should expose their hasher name via ``.django_name``. +DJANGO_PASSLIB_PREFIX = "django_" + +# non-django-specific hashes which also expose ``.django_name``. +_other_django_hashes = ["hex_md5"] + +def passlib_to_hasher_name(passlib_name): + "convert passlib handler name -> hasher name" + handler = get_crypt_handler(passlib_name) + if hasattr(handler, "django_name"): + return handler.django_name + return PASSLIB_HASHER_PREFIX + passlib_name + +def hasher_to_passlib_name(hasher_name): + "convert hasher name -> passlib handler name" + if hasher_name.startswith(PASSLIB_HASHER_PREFIX): + return hasher_name[len(PASSLIB_HASHER_PREFIX):] + for name in list_crypt_handlers(): + if name.startswith(DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes: + handler = get_crypt_handler(name) + if getattr(handler, "django_name", None) == hasher_name: + return name + # XXX: this should only happen for custom hashers that have been registered. + # _HasherHandler (below) is work in progress that would fix this. + raise ValueError("can't translate hasher name to passlib name: %r" % + hasher_name) + +#============================================================================= +# wrapping passlib handlers as django hashers +#============================================================================= +_FAKE_SALT = "--fake-salt--" + +class _HasherWrapper(object): + """helper for wrapping passlib handlers in Hasher-compatible class.""" + + # filled in by subclass, drives the other methods. + passlib_handler = None + + @classproperty + def algorithm(cls): + assert not hasattr(cls.passlib_handler, "django_name") + return PASSLIB_HASHER_PREFIX + cls.passlib_handler.name + + def salt(self): + # XXX: our encode wrapper generates a new salt each time it's called, + # so just returning a 'no value' flag here. + return _FAKE_SALT + + def verify(self, password, encoded): + return self.passlib_handler.verify(password, encoded) + + def encode(self, password, salt=None, iterations=None): + kwds = {} + if salt is not None and salt != _FAKE_SALT: + kwds['salt'] = salt + if iterations is not None: + kwds['rounds'] = iterations + return self.passlib_handler.encrypt(password, **kwds) + + _translate_kwds = dict(checksum="hash", rounds="iterations") + + def safe_summary(self, encoded): + from django.contrib.auth.hashers import mask_hash, _, SortedDict + handler = self.passlib_handler + items = [ + # since this is user-facing, we're reporting passlib's name, + # without the distracting PASSLIB_HASHER_PREFIX prepended. + (_('algorithm'), handler.name), + ] + if hasattr(handler, "parsehash"): + kwds = handler.parsehash(encoded, sanitize=mask_hash) + for key, value in iteritems(kwds): + key = self._translate_kwds.get(key, key) + items.append((_(key), value)) + return SortedDict(items) + +# cache of hasher wrappers generated by get_passlib_hasher() +_hasher_cache = WeakKeyDictionary() + +def get_passlib_hasher(handler): + """create *Hasher*-compatible wrapper for specified passlib hash. + + This takes in the name of a passlib hash (or the handler object itself), + and returns a wrapper instance which should be compatible with + Django 1.4's Hashers framework. + + If the named hash corresponds to one of Django's builtin hashers, + an instance of the real hasher class will be returned. + + Note that the format of the handler won't be altered, + so will probably not be compatible with Django's algorithm format, + so the monkeypatch provided by this plugin must have been applied. + + .. note:: + This function requires Django 1.4 or later. """ - global _django_patch_state, _dam, _has_django0 - _import_django() - state = _django_patch_state - User = _dam.User - - # issue warning if something else monkeypatched User - # while our patch was applied. - if state is not None: - if um(User.set_password) is not state['user_set_password']: - warn("another library has patched " - "django.contrib.auth.models:User.set_password") - if um(User.check_password) is not state['user_check_password']: - warn("another library has patched" - "django.contrib.auth.models:User.check_password") - if _dam.check_password is not state['models_check_password']: - warn("another library has patched" - "django.contrib.auth.models:check_password") - - #check if we should just restore original state - if context is None: - if state is not None: - del User.password_context - _dam.check_password = state['orig_models_check_password'] - User.set_password = state['orig_user_set_password'] - User.check_password = state['orig_user_check_password'] - _django_patch_state = None - return - - #validate inputs - if not is_crypt_context(context): - raise TypeError("context must be CryptContext instance or None: %r" % - (type(context),)) - - #backup original state if this is first call - if state is None: - _django_patch_state = state = dict( - orig_user_check_password = um(User.check_password), - orig_user_set_password = um(User.set_password), - orig_models_check_password = _dam.check_password, - ) - - #prepare replacements - if _has_django0: - UNUSABLE_PASSWORD = "!" + if DJANGO_VERSION < (1,4): + raise RuntimeError("get_passlib_hasher() requires Django >= 1.4") + if isinstance(handler, str): + handler = get_crypt_handler(handler) + if hasattr(handler, "django_name"): + # return native hasher instance + # XXX: should cache this too. + return _get_hasher(handler.django_name) + if handler.name == "django_disabled": + raise ValueError("can't wrap unusable-password handler") + try: + return _hasher_cache[handler] + except KeyError: + name = "Passlib_%s_PasswordHasher" % handler.name.title() + cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler)) + hasher = _hasher_cache[handler] = cls() + return hasher + +def _get_hasher(algorithm): + "wrapper to call django.contrib.auth.hashers:get_hasher()" + import sys + module = sys.modules.get("passlib.ext.django.models") + if module is None: + # we haven't patched django, so just import directly + from django.contrib.auth.hashers import get_hasher else: - UNUSABLE_PASSWORD = _dam.UNUSABLE_PASSWORD - - def set_password(user, raw_password): - "passlib replacement for User.set_password()" - if raw_password is None: - if _has_django0: - # django 0.9 - user.password = UNUSABLE_PASSWORD + # we've patched django, so have to use patch manager to retreive + # original get_hasher() function... + get_hasher = module._manager.getorig("django.contrib.auth.hashers:get_hasher") + return get_hasher(algorithm) + +#============================================================================= +# adapting django hashers -> passlib handlers +#============================================================================= +# TODO: this code probably halfway works, mainly just needs +# a routine to read HASHERS and PREFERRED_HASHER. + +##from passlib.registry import register_crypt_handler +##from passlib.utils import classproperty, to_native_str, to_unicode +##from passlib.utils.compat import unicode +## +## +##class _HasherHandler(object): +## "helper for wrapping Hasher instances as passlib handlers" +## # FIXME: this generic wrapper doesn't handle custom settings +## # FIXME: genconfig / genhash not supported. +## +## def __init__(self, hasher): +## self.django_hasher = hasher +## if hasattr(hasher, "iterations"): +## # assume encode() accepts an "iterations" parameter. +## # fake min/max rounds +## self.min_rounds = 1 +## self.max_rounds = 0xFFFFffff +## self.default_rounds = self.django_hasher.iterations +## self.setting_kwds += ("rounds",) +## +## # hasher instance - filled in by constructor +## django_hasher = None +## +## setting_kwds = ("salt",) +## context_kwds = () +## +## @property +## def name(self): +## # XXX: need to make sure this wont' collide w/ builtin django hashes. +## # maybe by renaming this to django compatible aliases? +## return DJANGO_PASSLIB_PREFIX + self.django_name +## +## @property +## def django_name(self): +## # expose this so hasher_to_passlib_name() extracts original name +## return self.django_hasher.algorithm +## +## @property +## def ident(self): +## # this should always be correct, as django relies on ident prefix. +## return unicode(self.django_name + "$") +## +## @property +## def identify(self, hash): +## # this should always work, as django relies on ident prefix. +## return to_unicode(hash, "latin-1", "hash").startswith(self.ident) +## +## @property +## def genconfig(self): +## # XXX: not sure how to support this. +## return None +## +## @property +## def genhash(self, secret, config): +## if config is not None: +## # XXX: not sure how to support this. +## raise NotImplementedError("genhash() for hashers not implemented") +## return self.encrypt(secret) +## +## @property +## def encrypt(self, secret, salt=None, **kwds): +## # NOTE: from how make_password() is coded, all hashers +## # should have salt param. but only some will have +## # 'iterations' parameter. +## opts = {} +## if 'rounds' in self.setting_kwds and 'rounds' in kwds: +## opts['iterations'] = kwds.pop("rounds") +## if kwds: +## raise TypeError("unexpected keyword arguments: %r" % list(kwds)) +## if isinstance(secret, unicode): +## secret = secret.encode("utf-8") +## if salt is None: +## salt = self.django_hasher.salt() +## return to_native_str(self.django_hasher(secret, salt, **opts)) +## +## @property +## def verify(self, secret, hash): +## hash = to_native_str(hash, "utf-8", "hash") +## if isinstance(secret, unicode): +## secret = secret.encode("utf-8") +## return self.django_hasher.verify(secret, hash) +## +##def register_hasher(hasher): +## handler = _HasherHandler(hasher) +## register_crypt_handler(handler) +## return handler + +#============================================================================= +# monkeypatch helpers +#============================================================================= +# private singleton indicating lack-of-value +_UNSET = object() + +class _PatchManager(object): + "helper to manage monkeypatches and run sanity checks" + + # NOTE: this could easily use a dict interface, + # but keeping it distinct to make clear that it's not a dict, + # since it has important side-effects. + + #=================================================================== + # init and support + #=================================================================== + def __init__(self, log=None): + # map of key -> (original value, patched value) + # original value may be _UNSET + self.log = log or logging.getLogger(__name__ + "._PatchManager") + self._state = {} + + # bool value tests if any patches are currently applied. + __bool__ = __nonzero__ = lambda self: bool(self._state) + + def _import_path(self, path): + "retrieve obj and final attribute name from resource path" + name, attr = path.split(":") + obj = __import__(name, fromlist=[attr], level=0) + while '.' in attr: + head, attr = attr.split(".", 1) + obj = getattr(obj, head) + return obj, attr + + @staticmethod + def _is_same_value(left, right): + "check if two values are the same (stripping method wrappers, etc)" + return get_method_function(left) == get_method_function(right) + + #=================================================================== + # reading + #=================================================================== + def _get_path(self, key, default=_UNSET): + obj, attr = self._import_path(key) + return getattr(obj, attr, default) + + def get(self, path, default=None): + "return current value for path" + return self._get_path(path, default) + + def getorig(self, path, default=None): + "return original (unpatched) value for path" + try: + value, _= self._state[path] + except KeyError: + value = self._get_path(path) + return default if value is _UNSET else value + + def check_all(self, strict=False): + """run sanity check on all keys, issue warning if out of sync""" + same = self._is_same_value + for path, (orig, expected) in iteritems(self._state): + if same(self._get_path(path), expected): + continue + msg = "another library has patched resource: %r" % path + if strict: + raise RuntimeError(msg) else: - user.set_unusable_password() - else: - cat = get_category(user) if get_category else None - user.password = context.encrypt(raw_password, category=cat) + warn(msg, PasslibRuntimeWarning) - def check_password(user, raw_password): - "passlib replacement for User.check_password()" - if raw_password is None: - return False - hash = user.password - if not hash or hash == UNUSABLE_PASSWORD: - return False - cat = get_category(user) if get_category else None - ok, new_hash = context.verify_and_update(raw_password, hash, - category=cat) - if ok and new_hash is not None: - user.password = new_hash - user.save() - return ok - - def raw_check_password(raw_password, enc_password): - "passlib replacement for check_password()" - if not enc_password or enc_password == UNUSABLE_PASSWORD: - raise ValueError("no password hash specified") - return context.verify(raw_password, enc_password) - - #set new state - User.password_context = context - User.set_password = state['user_set_password'] = set_password - User.check_password = state['user_check_password'] = check_password - _dam.check_password = state['models_check_password'] = raw_check_password - state['context' ] = context - state['get_category'] = get_category - -##def get_django_password_context(): -## """return current django password context -## -## This returns the current :class:`~passlib.context.CryptContext` instance -## set by :func:`set_django_password_context`. -## If not context has been set, returns ``None``. -## """ -## global _django_patch_state -## if _django_patch_state: -## return _django_patch_state['context'] -## else: -## return None + #=================================================================== + # patching + #=================================================================== + def _set_path(self, path, value): + obj, attr = self._import_path(path) + if value is _UNSET: + if hasattr(obj, attr): + delattr(obj, attr) + else: + setattr(obj, attr, value) -#=================================================================== -#eof -#=================================================================== + def patch(self, path, value): + "monkeypatch object+attr at to have , stores original" + assert value != _UNSET + current = self._get_path(path) + try: + orig, expected = self._state[path] + except KeyError: + self.log.debug("patching resource: %r", path) + orig = current + else: + self.log.debug("modifying resource: %r", path) + if not self._is_same_value(current, expected): + warn("overridding resource another library has patched: %r" + % path, PasslibRuntimeWarning) + self._set_path(path, value) + self._state[path] = (orig, value) + + ##def patch_many(self, **kwds): + ## "override specified resources with new values" + ## for path, value in iteritems(kwds): + ## self.patch(path, value) + + def monkeypatch(self, parent, name=None, enable=True): + "function decorator which patches function of same name in " + def builder(func): + if enable: + sep = "." if ":" in parent else ":" + path = parent + sep + (name or func.__name__) + self.patch(path, func) + return func + return builder + + #=================================================================== + # unpatching + #=================================================================== + def unpatch(self, path, unpatch_conflicts=True): + try: + orig, expected = self._state[path] + except KeyError: + return + current = self._get_path(path) + self.log.debug("unpatching resource: %r", path) + if not self._is_same_value(current, expected): + if unpatch_conflicts: + warn("reverting resource another library has patched: %r" + % path, PasslibRuntimeWarning) + else: + warn("not reverting resource another library has patched: %r" + % path, PasslibRuntimeWarning) + del self._state[path] + return + self._set_path(path, orig) + del self._state[path] + + def unpatch_all(self, **kwds): + for key in list(self._state): + self.unpatch(key, **kwds) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/__init__.py passlib-1.6.1/passlib/handlers/__init__.py --- passlib-1.5.3/passlib/handlers/__init__.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/__init__.py 2012-05-03 16:36:58.000000000 +0000 @@ -1,3 +1 @@ -#XXX: make this a namespace package ? - -#TODO: bigcrypt, crypt16, raw hex strings for md5/sha1/sha256/sha512 +"""passlib.handlers -- holds implementations of all passlib's builtin hash formats""" diff -Nru passlib-1.5.3/passlib/handlers/bcrypt.py passlib-1.6.1/passlib/handlers/bcrypt.py --- passlib-1.5.3/passlib/handlers/bcrypt.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/bcrypt.py 2012-08-02 15:46:11.000000000 +0000 @@ -1,200 +1,237 @@ -"""passlib.bcrypt +"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm. -Implementation of OpenBSD's BCrypt algorithm. +TODO: -PassLib will use the py-bcrypt package if it is available, -otherwise it will fall back to a slower builtin pure-python implementation. +* support 2x and altered-2a hashes? + http://www.openwall.com/lists/oss-security/2011/06/27/9 -Note that rounds must be >= 10 or an error will be returned. +* deal with lack of PY3-compatibile c-ext implementation """ -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement, absolute_import -#core +# core +import os import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site +# site try: from bcrypt import hashpw as pybcrypt_hashpw -except ImportError: #pragma: no cover - though should run whole suite w/o pybcrypt installed +except ImportError: # pragma: no cover pybcrypt_hashpw = None try: from bcryptor.engine import Engine as bcryptor_engine -except ImportError: #pragma: no cover - though should run whole suite w/o bcryptor installed +except ImportError: # pragma: no cover bcryptor_engine = None -#libs -from passlib.utils import safe_os_crypt, classproperty, handlers as uh, \ - h64, to_hash_str, rng, getrandstr, bytes +# pkg +from passlib.exc import PasslibHashWarning +from passlib.utils import bcrypt64, safe_crypt, repeat_string, \ + classproperty, rng, getrandstr, test_crypt +from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii +import passlib.utils.handlers as uh -#pkg -#local +# local __all__ = [ "bcrypt", ] -# base64 character->value mapping used by bcrypt. -# this is same as as H64_CHARS, but the positions are different. -BCHARS = u"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - -# last bcrypt salt char should have 4 padding bits set to 0. -# thus, only the following chars are allowed: -BSLAST = u".Oeu" -BHLAST = u'.CGKOSWaeimquy26' - -#========================================================= -#handler -#========================================================= +#============================================================================= +# support funcs & constants +#============================================================================= +_builtin_bcrypt = None + +def _load_builtin(): + global _builtin_bcrypt + if _builtin_bcrypt is None: + from passlib.utils._blowfish import raw_bcrypt as _builtin_bcrypt + +IDENT_2 = u("$2$") +IDENT_2A = u("$2a$") +IDENT_2X = u("$2x$") +IDENT_2Y = u("$2y$") +_BNULL = b('\x00') + +#============================================================================= +# handler +#============================================================================= class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.GenericHandler): """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 12, must be between 4 and 31, inclusive. - This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`. + This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}` + -- increasing the rounds by +1 will double the amount of time taken. + :type ident: str :param ident: - selects specific version of BCrypt hash that will be used. - Typically you want to leave this alone, and let it default to ``2a``, - but it can be set to ``2`` to use the older version of BCrypt. - - It will use the first available of three possible backends: - - 1. `py-bcrypt `_, if installed. - 2. `bcryptor `_, if installed. - 3. stdlib's :func:`crypt.crypt()`, if the host OS supports BCrypt (eg: BSD). - - If no backends are available at runtime, - :exc:`~passlib.utils.MissingBackendError` will be raised - whenever :meth:`encrypt` or :meth:`verify` are called. - You can see which backend is in use by calling the - :meth:`~passlib.utils.handlers.HasManyBackends.get_backend()` method. + Specifies which version of the BCrypt algorithm will be used when creating a new hash. + Typically this option is not needed, as the default (``"2a"``) is usually the correct choice. + If specified, it must be one of the following: + + * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore. + * ``"2a"`` - latest revision of the official BCrypt algorithm, and the current default. + * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, + identical to ``"2a"`` in all but name. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This class now supports ``"2y"`` hashes, and recognizes + (but does not support) the broken ``"2x"`` hashes. + (see the :ref:`crypt_blowfish bug ` + for details). + + .. versionchanged:: 1.6 + Added a pure-python backend. """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "bcrypt" setting_kwds = ("salt", "rounds", "ident") checksum_size = 31 - checksum_chars = BCHARS + checksum_chars = bcrypt64.charmap #--HasManyIdents-- - default_ident = u"$2a$" - ident_values = (u"$2$", u"$2a$") - ident_aliases = {u"2": u"$2$", u"2a": u"$2a$"} + default_ident = u("$2a$") + ident_values = (u("$2$"), IDENT_2A, IDENT_2X, IDENT_2Y) + ident_aliases = {u("2"): u("$2$"), u("2a"): IDENT_2A, u("2y"): IDENT_2Y} #--HasSalt-- min_salt_size = max_salt_size = 22 - salt_chars = BCHARS - #NOTE: 22nd salt char must be in BSLAST, not full BCHARS + salt_chars = bcrypt64.charmap + # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap #--HasRounds-- - default_rounds = 12 #current passlib default + default_rounds = 12 # current passlib default min_rounds = 4 # bcrypt spec specified minimum max_rounds = 31 # 32-bit integer limit (since real_rounds=1< ascii bytes (we override this) - # unicode hash -> ascii bytes (we provide ascii bytes) - # returns ascii bytes - # py3: can't get to install + # py-bcrypt behavior: + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes taken as-is; returns ascii bytes. + # py3: not supported (patch submitted) if isinstance(secret, unicode): secret = secret.encode("utf-8") - hash = pybcrypt_hashpw(secret, - self.to_string(native=False)) - return hash[-31:].decode("ascii") + if _BNULL in secret: + raise uh.exc.NullPasswordError(self) + config = self._get_config() + hash = pybcrypt_hashpw(secret, config) + assert hash.startswith(config) and len(hash) == len(config)+31 + return str_to_uascii(hash[-31:]) def _calc_checksum_bcryptor(self, secret): - #bcryptor behavior: - # py2: unicode secret -> ascii bytes (we have to override) - # unicode hash -> ascii bytes (we provide ascii bytes) - # returns ascii bytes - # py3: can't get to install + # bcryptor behavior: + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes taken as-is; returns ascii bytes. + # py3: not supported + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _BNULL in secret: + # NOTE: especially important to forbid NULLs for bcryptor, + # since it happily accepts them, and then silently truncates + # the password at first one it encounters :( + raise uh.exc.NullPasswordError(self) + if self.ident == IDENT_2: + # bcryptor doesn't support $2$ hashes; but we can fake $2$ behavior + # using the $2a$ algorithm, by repeating the password until + # it's at least 72 chars in length. + if secret: + secret = repeat_string(secret, 72) + config = self._get_config(IDENT_2A) + else: + config = self._get_config() + hash = bcryptor_engine(False).hash_key(secret, config) + assert hash.startswith(config) and len(hash) == len(config)+31 + return str_to_uascii(hash[-31:]) + + def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - hash = bcryptor_engine(False).hash_key(secret, - self.to_string(native=False)) - return hash[-31:].decode("ascii") - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + if _BNULL in secret: + raise uh.exc.NullPasswordError(self) + chk = _builtin_bcrypt(secret, self.ident.strip("$"), + self.salt.encode("ascii"), self.rounds) + return chk.decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/cisco.py passlib-1.6.1/passlib/handlers/cisco.py --- passlib-1.5.3/passlib/handlers/cisco.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/cisco.py 2012-08-01 17:10:03.000000000 +0000 @@ -0,0 +1,217 @@ +"""passlib.handlers.cisco - Cisco password hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import md5 +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import h64, right_pad_string, to_unicode +from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, join_byte_values, \ + join_byte_elems, byte_elem_value, iter_byte_values, uascii_to_str, str_to_uascii +import passlib.utils.handlers as uh +# local +__all__ = [ + "cisco_pix", + "cisco_type7", +] + +#============================================================================= +# cisco pix firewall hash +#============================================================================= +class cisco_pix(uh.HasUserContext, uh.StaticHandler): + """This class implements the password hash used by Cisco PIX firewalls, + and follows the :ref:`password-hash-api`. + It does a single round of hashing, and relies on the username + as the salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods + have the following extra keyword: + + :type user: str + :param user: + String containing name of user account this password is associated with. + + This is *required* in order to correctly hash passwords associated + with a user account on the Cisco device, as it is used to salt + the hash. + + Conversely, this *must* be omitted or set to ``""`` in order to correctly + hash passwords which don't have an associated user account + (such as the "enable" password). + """ + #=================================================================== + # class attrs + #=================================================================== + name = "cisco_pix" + checksum_size = 16 + checksum_chars = uh.HASH64_CHARS + + #=================================================================== + # methods + #=================================================================== + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + # XXX: no idea what unicode policy is, but all examples are + # 7-bit ascii compatible, so using UTF-8 + secret = secret.encode("utf-8") + + user = self.user + if user: + # NOTE: not *positive* about this, but it looks like per-user + # accounts use first 4 chars of user as salt, whereas global + # "enable" passwords don't have any salt at all. + if isinstance(user, unicode): + user = user.encode("utf-8") + secret += user[:4] + + # pad/truncate to 16 + secret = right_pad_string(secret, 16) + + # md5 digest + hash = md5(secret).digest() + + # drop every 4th byte + hash = join_byte_elems(c for i,c in enumerate(hash) if i & 3 < 3) + + # encode using Hash64 + return h64.encode_bytes(hash).decode("ascii") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# type 7 +#============================================================================= +class cisco_type7(uh.GenericHandler): + """This class implements the Type 7 password encoding used by Cisco IOS, + and follows the :ref:`password-hash-api`. + It has a simple 4-5 bit salt, but is nonetheless a reversible encoding + instead of a real hash. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genhash` methods + have the following optional keywords: + + :type salt: int + :param salt: + This may be an optional salt integer drawn from ``range(0,16)``. + If omitted, one will be chosen at random. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` values that are out of range. + + Note that while this class outputs digests in upper-case hexidecimal, + it will accept lower-case as well. + + This class also provides the following additional method: + + .. automethod:: decode + """ + #=================================================================== + # class attrs + #=================================================================== + name = "cisco_type7" + setting_kwds = ("salt",) + checksum_chars = uh.UPPER_HEX_CHARS + + min_salt_value = 0 + max_salt_value = 52 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config): + # special case to handle ``config=None`` in same style as StaticHandler + if config is None: + return cls.encrypt(secret) + else: + return super(cisco_type7, cls).genhash(secret, config) + + @classmethod + def from_string(cls, hash): + hash = to_unicode(hash, "ascii", "hash") + if len(hash) < 2: + raise uh.exc.InvalidHashError(cls) + salt = int(hash[:2]) # may throw ValueError + return cls(salt=salt, checksum=hash[2:].upper()) + + def __init__(self, salt=None, **kwds): + super(cisco_type7, self).__init__(**kwds) + self.salt = self._norm_salt(salt) + + def _norm_salt(self, salt): + # NOTE: the "salt" for this algorithm is a small integer. + # XXX: not entirely sure that values >15 are valid, so for + # compatibility we don't output those values but we do accept them. + if salt is None: + if self.use_defaults: + salt = self._generate_salt() + else: + raise TypeError("no salt specified") + if not isinstance(salt, int): + raise uh.exc.ExpectedTypeError(salt, "integer", "salt") + if salt < 0 or salt > self.max_salt_value: + msg = "salt/offset must be in 0..52 range" + if self.relaxed: + warn(msg, uh.PasslibHashWarning) + salt = 0 if salt < 0 else self.max_salt_value + else: + raise ValueError(msg) + return salt + + def _generate_salt(self): + return uh.rng.randint(0, 15) + + def to_string(self): + return "%02d%s" % (self.salt, uascii_to_str(self.checksum)) + + def _calc_checksum(self, secret): + # XXX: no idea what unicode policy is, but all examples are + # 7-bit ascii compatible, so using UTF-8 + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper() + + @classmethod + def decode(cls, hash, encoding="utf-8"): + """decode hash, returning original password. + + :arg hash: encoded password + :param encoding: optional encoding to use (defaults to ``UTF-8``). + :returns: password as unicode + """ + self = cls.from_string(hash) + tmp = unhexlify(self.checksum.encode("ascii")) + raw = self._cipher(tmp, self.salt) + return raw.decode(encoding) if encoding else raw + + # type7 uses a xor-based vingere variant, using the following secret key: + _key = u("dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87") + + @classmethod + def _cipher(cls, data, salt): + "xor static key against data - encrypts & decrypts" + key = cls._key + key_size = len(key) + return join_byte_values( + value ^ ord(key[(salt + idx) % key_size]) + for idx, value in enumerate(iter_byte_values(data)) + ) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/des_crypt.py passlib-1.6.1/passlib/handlers/des_crypt.py --- passlib-1.5.3/passlib/handlers/des_crypt.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/des_crypt.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,68 +1,18 @@ -"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants - -.. note:: - - for des-crypt, passlib restricts salt characters to just the hash64 charset, - and salt string size to >= 2 chars; since implementations of des-crypt - vary in how they handle other characters / sizes... - - linux - - linux crypt() accepts salt characters outside the hash64 charset, - and maps them using the following formula (determined by examining crypt's output): - chr 0..64: v = (c-(1-19)) & 63 = (c+18) & 63 - chr 65..96: v = (c-(65-12)) & 63 = (c+11) & 63 - chr 97..127: v = (c-(97-38)) & 63 = (c+5) & 63 - chr 128..255: same as c-128 - - invalid salt chars are mirrored back in the resulting hash. - - if the salt is too small, it uses a NUL char for the remaining - character (which is treated the same as the char ``G``) - when decoding the 12 bit salt. however, it outputs - a hash string containing the single salt char twice, - resulting in a corrupted hash. - - netbsd - - netbsd crypt() uses a 128-byte lookup table, - which is only initialized for the hash64 values. - the remaining values < 128 are implicitly zeroed, - and values > 128 access past the array bounds - (but seem to return 0). - - if the salt string is too small, it reads - the NULL char (and continues past the end for bsdi crypt, - though the buffer is usually large enough and NULLed). - salt strings are output as provided, - except for any NULs, which are converted to ``.``. - - openbsd, freebsd - - openbsd crypt() strictly defines the hash64 values as normal, - and all other char values as 0. salt chars are reported as provided. - - if the salt or rounds string is too small, - it'll read past the end, resulting in unpredictable - values, though it'll terminate it's encoding - of the output at the first null. - this will generally result in a corrupted hash. -""" - -#========================================================= -#imports -#========================================================= -#core +"""passlib.handlers.des_crypt - traditional unix (DES) crypt and variants""" +#============================================================================= +# imports +#============================================================================= +# core import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import h64, classproperty, safe_os_crypt, b, bytes, \ - to_hash_str, handlers as uh, bord -from passlib.utils.des import mdes_encrypt_int_block -#pkg -#local +# site +# pkg +from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt, to_unicode +from passlib.utils.compat import b, bytes, byte_elem_value, u, uascii_to_str, unicode +from passlib.utils.des import des_encrypt_int_block +import passlib.utils.handlers as uh +# local __all__ = [ "des_crypt", "bsdi_crypt", @@ -70,473 +20,498 @@ "crypt16", ] -#========================================================= -#pure-python backend -#========================================================= +#============================================================================= +# pure-python backend for des_crypt family +#============================================================================= +_BNULL = b('\x00') + def _crypt_secret_to_key(secret): - "crypt helper which converts lower 7 bits of first 8 chars of secret -> 56-bit des key, padded to 64 bits" - return sum( - (bord(c) & 0x7f) << (57-8*i) - for i, c in enumerate(secret[:8]) - ) + """convert secret to 64-bit DES key. -def raw_crypt(secret, salt): - "pure-python fallback if stdlib support not present" + this only uses the first 8 bytes of the secret, + and discards the high 8th bit of each byte at that. + a null parity bit is inserted after every 7th bit of the output. + """ + # NOTE: this would set the parity bits correctly, + # but des_encrypt_int_block() would just ignore them... + ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8) + ## for i, c in enumerate(secret[:8])) + return sum((byte_elem_value(c) & 0x7f) << (57-i*8) + for i, c in enumerate(secret[:8])) + +def _raw_des_crypt(secret, salt): + "pure-python backed for des_crypt" assert len(salt) == 2 - #NOTE: technically could accept non-standard salts & single char salt, - #but no official spec. + # NOTE: some OSes will accept non-HASH64 characters in the salt, + # but what value they assign these characters varies wildy, + # so just rejecting them outright. + # NOTE: the same goes for single-character salts... + # some OSes duplicate the char, some insert a '.' char, + # and openbsd does something which creates an invalid hash. try: salt_value = h64.decode_int12(salt) - except ValueError: #pragma: no cover - always caught by class + except ValueError: # pragma: no cover - always caught by class raise ValueError("invalid chars in salt") - #FIXME: ^ this will throws error if bad salt chars are used - # whereas linux crypt does something (inexplicable) with it - - #convert first 8 bytes of secret string into an integer - key_value = _crypt_secret_to_key(secret) - - #run data through des using input of 0 - result = mdes_encrypt_int_block(key_value, 0, salt_value, 25) - #run h64 encode on result - return h64.encode_dc_int64(result) + # gotta do something - no official policy since this predates unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + + # forbidding NULL char because underlying crypt() rejects them too. + if _BNULL in secret: + raise uh.exc.NullPasswordError(des_crypt) -def raw_ext_crypt(secret, rounds, salt): - "ext_crypt() helper which returns checksum only" + # convert first 8 bytes of secret string into an integer + key_value = _crypt_secret_to_key(secret) - #decode salt - try: - salt_value = h64.decode_int24(salt) - except ValueError: #pragma: no cover - always caught by class - raise ValueError("invalid salt") + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, 25) - #validate secret - if b('\x00') in secret: #pragma: no cover - always caught by class - #builtin linux crypt doesn't like this, so we don't either - #XXX: would make more sense to raise ValueError, but want to be compatible w/ stdlib crypt - raise ValueError("secret must be string without null bytes") + # run h64 encode on result + return h64big.encode_int64(result) - #convert secret string into an integer +def _bsdi_secret_to_key(secret): + "covert secret to DES key used by bsdi_crypt" key_value = _crypt_secret_to_key(secret) idx = 8 end = len(secret) while idx < end: next = idx+8 - key_value = mdes_encrypt_int_block(key_value, key_value) ^ \ - _crypt_secret_to_key(secret[idx:next]) + tmp_value = _crypt_secret_to_key(secret[idx:next]) + key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value idx = next + return key_value - #run data through des using input of 0 - result = mdes_encrypt_int_block(key_value, 0, salt_value, rounds) +def _raw_bsdi_crypt(secret, rounds, salt): + "pure-python backend for bsdi_crypt" - #run h64 encode on result - return h64.encode_dc_int64(result) + # decode salt + try: + salt_value = h64.decode_int24(salt) + except ValueError: # pragma: no cover - always caught by class + raise ValueError("invalid salt") -#========================================================= -#handler -#========================================================= + # gotta do something - no official policy since this predates unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + assert isinstance(secret, bytes) + + # forbidding NULL char because underlying crypt() rejects them too. + if _BNULL in secret: + raise uh.exc.NullPasswordError(bsdi_crypt) + + # convert secret string into an integer + key_value = _bsdi_secret_to_key(secret) + + # run data through des using input of 0 + result = des_encrypt_int_block(key_value, 0, salt_value, rounds) + + # run h64 encode on result + return h64big.encode_int64(result) + +#============================================================================= +# handlers +#============================================================================= class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. - It will use the first available of two possible backends: + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. - * stdlib :func:`crypt()`, if the host OS supports des-crypt (most unix systems). - * a pure python implementation of des-crypt - - You can see which backend is in use by calling the :meth:`get_backend()` method. + .. versionadded:: 1.6 """ - - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "des_crypt" setting_kwds = ("salt",) - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS + checksum_size = 11 #--HasSalt-- min_salt_size = max_salt_size = 2 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - #========================================================= - #formatting - #========================================================= - #FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum + #=================================================================== + # formatting + #=================================================================== + # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum - _pat = re.compile(ur""" + _hash_regex = re.compile(u(r""" ^ (?P[./a-z0-9]{2}) (?P[./a-z0-9]{11})? - $""", re.X|re.I) - - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + $"""), re.X|re.I) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") + hash = to_unicode(hash, "ascii", "hash") salt, chk = hash[:2], hash[2:] - return cls(salt=salt, checksum=chk, strict=bool(chk)) + return cls(salt=salt, checksum=chk or None) - def to_string(self, native=True): - hash = u"%s%s" % (self.salt, self.checksum or u'') - return to_hash_str(hash) if native else hash - - #========================================================= - #backend - #========================================================= + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== backends = ("os_crypt", "builtin") _has_backend_builtin = True @classproperty def _has_backend_os_crypt(cls): - h = u'abgOeLfPimXQo' - return bool(safe_os_crypt and safe_os_crypt(u"test",h)[1]==h) + return test_crypt("test", 'abgOeLfPimXQo') def _calc_checksum_builtin(self, secret): - #gotta do something - no official policy since des-crypt predates unicode - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - #forbidding nul chars because linux crypt (and most C implementations) won't accept it either. - if b('\x00') in secret: - raise ValueError("null char in secret") - return raw_crypt(secret, self.salt.encode("ascii")).decode("ascii") + return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): - #os_crypt() would raise less useful error - null = u'\x00' if isinstance(secret, unicode) else b('\x00') - if null in secret: - raise ValueError("null char in secret") - - #NOTE: safe_os_crypt encodes unicode secret -> utf8 - #no official policy since des-crypt predates unicode - ok, hash = safe_os_crypt(secret, self.salt) - if ok: + # NOTE: safe_crypt encodes unicode secret -> utf8 + # no official policy since des-crypt predates unicode + hash = safe_crypt(secret, self.salt) + if hash: + assert hash.startswith(self.salt) and len(hash) == 13 return hash[2:] else: return self._calc_checksum_builtin(secret) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#handler -#========================================================= - -#FIXME: phpass code notes that even rounds values should be avoided for BSDI-Crypt, -# so as not to reveal weak des keys. given the random salt, this shouldn't be -# a very likely issue anyways, but should do something about default rounds generation anyways. + #=================================================================== + # eoc + #=================================================================== class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + :type rounds: int :param rounds: Optional number of rounds to use. - Defaults to 5000, must be between 0 and 16777215, inclusive. - - It will use the first available of two possible backends: + Defaults to 5001, must be between 1 and 16777215, inclusive. - * stdlib :func:`crypt()`, if the host OS supports bsdi-crypt (most BSD systems). - * a pure python implementation of bsdi-crypt - - You can see which backend is in use by calling the :meth:`get_backend()` method. + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + :meth:`encrypt` will now issue a warning if an even number of rounds is used + (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys). """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "bsdi_crypt" setting_kwds = ("salt", "rounds") checksum_size = 11 - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 4 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS #--HasRounds-- default_rounds = 5001 - min_rounds = 0 + min_rounds = 1 max_rounds = 16777215 # (1<<24)-1 rounds_cost = "linear" # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds, # but that seems to be an OS policy, not a algorithm limitation. - #========================================================= - #internal helpers - #========================================================= - _pat = re.compile(ur""" + #=================================================================== + # parsing + #=================================================================== + _hash_regex = re.compile(u(r""" ^ _ (?P[./a-z0-9]{4}) (?P[./a-z0-9]{4}) (?P[./a-z0-9]{11})? - $""", re.X|re.I) - - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + $"""), re.X|re.I) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._pat.match(hash) + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) if not m: - raise ValueError("invalid ext-des-crypt hash") + raise uh.exc.InvalidHashError(cls) rounds, salt, chk = m.group("rounds", "salt", "chk") return cls( rounds=h64.decode_int24(rounds.encode("ascii")), salt=salt, checksum=chk, - strict=bool(chk), ) - def to_string(self, native=True): - hash = u"_%s%s%s" % (h64.encode_int24(self.rounds).decode("ascii"), - self.salt, self.checksum or u'') - return to_hash_str(hash) if native else hash - - #========================================================= - #backend - #========================================================= + def to_string(self): + hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), + self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # validation + #=================================================================== + + # flag so CryptContext won't generate even rounds. + _avoid_even_rounds = True + + def _norm_rounds(self, rounds): + rounds = super(bsdi_crypt, self)._norm_rounds(rounds) + # issue warning if app provided an even rounds value + if self.use_defaults and not rounds & 1: + warn("bsdi_crypt rounds should be odd, " + "as even rounds may reveal weak DES keys", + uh.exc.PasslibSecurityWarning) + return rounds + + @classmethod + def _bind_needs_update(cls, **settings): + return cls._needs_update + + @classmethod + def _needs_update(cls, hash, secret): + # mark bsdi_crypt hashes as deprecated if they have even rounds. + assert cls.identify(hash) + if isinstance(hash, unicode): + hash = hash.encode("ascii") + rounds = h64.decode_int24(hash[1:5]) + return not rounds & 1 + + #=================================================================== + # backends + #=================================================================== backends = ("os_crypt", "builtin") _has_backend_builtin = True @classproperty def _has_backend_os_crypt(cls): - h = u'_/...lLDAxARksGCHin.' - return bool(safe_os_crypt and safe_os_crypt(u"test",h)[1]==h) - + return test_crypt("test", '_/...lLDAxARksGCHin.') + def _calc_checksum_builtin(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - return raw_ext_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") + return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.to_string(native=False)) - if ok: - return hash[9:] + config = self.to_string() + hash = safe_crypt(secret, config) + if hash: + assert hash.startswith(config[:9]) and len(hash) == 20 + return hash[-11:] else: return self._calc_checksum_builtin(secret) - #========================================================= - #eoc - #========================================================= - -#========================================================= -# -#========================================================= + #=================================================================== + # eoc + #=================================================================== + class bigcrypt(uh.HasSalt, uh.GenericHandler): """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "bigcrypt" setting_kwds = ("salt",) - checksum_chars = uh.H64_CHARS - #NOTE: checksum chars must be multiple of 11 + checksum_chars = uh.HASH64_CHARS + # NOTE: checksum chars must be multiple of 11 #--HasSalt-- min_salt_size = max_salt_size = 2 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - #========================================================= - #internal helpers - #========================================================= - _pat = re.compile(ur""" + #=================================================================== + # internal helpers + #=================================================================== + _hash_regex = re.compile(u(r""" ^ (?P[./a-z0-9]{2}) - (?P[./a-z0-9]{11,})? - $""", re.X|re.I) - - @classmethod - def identify(cls, hash): - if not hash: - return False - if isinstance(hash, bytes): - try: - hash = hash.decode("ascii") - except UnicodeDecodeError: - return False - return bool(cls._pat.match(hash)) and (len(hash)-2) % 11 == 0 + (?P([./a-z0-9]{11})+)? + $"""), re.X|re.I) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._pat.match(hash) + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) if not m: - raise ValueError("invalid bigcrypt hash") + raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") - return cls(salt=salt, checksum=chk, strict=bool(chk)) + return cls(salt=salt, checksum=chk) - def to_string(self, native=True): - hash = u"%s%s" % (self.salt, self.checksum or u'') - return to_hash_str(hash) if native else hash + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum or u('')) + return uascii_to_str(hash) - @classmethod - def norm_checksum(cls, value, strict=False): - value = super(bigcrypt, cls).norm_checksum(value, strict=strict) + def _norm_checksum(self, value): + value = super(bigcrypt, self)._norm_checksum(value) if value and len(value) % 11: - raise ValueError("invalid bigcrypt hash") + raise uh.exc.InvalidHashError(self) return value - #========================================================= - #backend - #========================================================= - #TODO: check if os_crypt supports ext-des-crypt. - - def calc_checksum(self, secret): + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - chk = raw_crypt(secret, self.salt.encode("ascii")) + chk = _raw_des_crypt(secret, self.salt.encode("ascii")) idx = 8 end = len(secret) while idx < end: next = idx + 8 - chk += raw_crypt(secret[idx:next], chk[-11:-9]) + chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) idx = next return chk.decode("ascii") - #========================================================= - #eoc - #========================================================= - -#========================================================= -# -#========================================================= + #=================================================================== + # eoc + #=================================================================== + class crypt16(uh.HasSalt, uh.GenericHandler): """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "crypt16" setting_kwds = ("salt",) checksum_size = 22 - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 2 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - #========================================================= - #internal helpers - #========================================================= - _pat = re.compile(ur""" + #=================================================================== + # internal helpers + #=================================================================== + _hash_regex = re.compile(u(r""" ^ (?P[./a-z0-9]{2}) (?P[./a-z0-9]{22})? - $""", re.X|re.I) - - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + $"""), re.X|re.I) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._pat.match(hash) + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) if not m: - raise ValueError("invalid crypt16 hash") + raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") - return cls(salt=salt, checksum=chk, strict=bool(chk)) - - def to_string(self, native=True): - hash = u"%s%s" % (self.salt, self.checksum or u'') - return to_hash_str(hash) if native else hash - - #========================================================= - #backend - #========================================================= - #TODO: check if os_crypt supports ext-des-crypt. + return cls(salt=salt, checksum=chk) - def calc_checksum(self, secret): + def to_string(self): + hash = u("%s%s") % (self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - #parse salt value + # parse salt value try: salt_value = h64.decode_int12(self.salt.encode("ascii")) - except ValueError: #pragma: no cover - caught by class + except ValueError: # pragma: no cover - caught by class raise ValueError("invalid chars in salt") - #convert first 8 byts of secret string into an integer, + # convert first 8 byts of secret string into an integer, key1 = _crypt_secret_to_key(secret) - #run data through des using input of 0 - result1 = mdes_encrypt_int_block(key1, 0, salt_value, 20) + # run data through des using input of 0 + result1 = des_encrypt_int_block(key1, 0, salt_value, 20) - #convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) - key2 = _crypt_secret_to_key(secret[8:]) + # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars) + key2 = _crypt_secret_to_key(secret[8:16]) - #run data through des using input of 0 - result2 = mdes_encrypt_int_block(key2, 0, salt_value, 5) + # run data through des using input of 0 + result2 = des_encrypt_int_block(key2, 0, salt_value, 5) - #done - chk = h64.encode_dc_int64(result1) + h64.encode_dc_int64(result2) + # done + chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) return chk.decode("ascii") - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/digests.py passlib-1.6.1/passlib/handlers/digests.py --- passlib-1.5.3/passlib/handlers/digests.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/digests.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,18 +1,19 @@ """passlib.handlers.digests - plain hash digests """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core import hashlib import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import handlers as uh, to_hash_str, bytes +# site +# pkg +from passlib.utils import to_native_str, to_bytes, render_bytes, consteq +from passlib.utils.compat import bascii_to_str, bytes, unicode, str_to_uascii +import passlib.utils.handlers as uh from passlib.utils.md4 import md4 -#pkg -#local +# local __all__ = [ "create_hex_hash", "hex_md4", @@ -22,50 +23,42 @@ "hex_sha512", ] -#========================================================= -#helpers for hexidecimal hashes -#========================================================= +#============================================================================= +# helpers for hexidecimal hashes +#============================================================================= class HexDigestHash(uh.StaticHandler): "this provides a template for supporting passwords stored as plain hexidecimal hashes" - _hash_func = None #required - hash function - checksum_size = None #required - size of encoded digest + #=================================================================== + # class attrs + #=================================================================== + _hash_func = None # hash function to use - filled in by create_hex_hash() + checksum_size = None # filled in by create_hex_hash() checksum_chars = uh.HEX_CHARS + #=================================================================== + # methods + #=================================================================== @classmethod - def identify(cls, hash): - if not hash: - return False - if isinstance(hash, bytes): - try: - hash = hash.decode("ascii") - except UnicodeDecodeError: - return False - cc = cls.checksum_chars - return len(hash) == cls.checksum_size and all(c in cc for c in hash) + def _norm_hash(cls, hash): + return hash.lower() - @classmethod - def genhash(cls, secret, hash): - if hash is not None and not cls.identify(hash): - raise ValueError("not a %s hash" % (cls.name,)) - if secret is None: - raise TypeError("no secret provided") + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - return to_hash_str(cls._hash_func(secret).hexdigest()) + return str_to_uascii(self._hash_func(secret).hexdigest()) - @classmethod - def _norm_hash(cls, hash): - if isinstance(hash, bytes): - hash = hash.decode("ascii") - return hash.lower() + #=================================================================== + # eoc + #=================================================================== -def create_hex_hash(hash, digest_name): - #NOTE: could set digest_name=hash.name for cpython, but not for some other platforms. +def create_hex_hash(hash, digest_name, module=__name__): + # NOTE: could set digest_name=hash.name for cpython, but not for some other platforms. h = hash() name = "hex_" + digest_name return type(name, (HexDigestHash,), dict( name=name, - _hash_func=staticmethod(hash), #sometimes it's a function, sometimes not. so wrap it. + __module__=module, # so ABCMeta won't clobber it + _hash_func=staticmethod(hash), # sometimes it's a function, sometimes not. so wrap it. checksum_size=h.digest_size*2, __doc__="""This class implements a plain hexidecimal %s hash, and follows the :ref:`password-hash-api`. @@ -73,15 +66,79 @@ """ % (digest_name,) )) -#========================================================= -#predefined handlers -#========================================================= +#============================================================================= +# predefined handlers +#============================================================================= hex_md4 = create_hex_hash(md4, "md4") hex_md5 = create_hex_hash(hashlib.md5, "md5") +hex_md5.django_name = "unsalted_md5" hex_sha1 = create_hex_hash(hashlib.sha1, "sha1") hex_sha256 = create_hex_hash(hashlib.sha256, "sha256") hex_sha512 = create_hex_hash(hashlib.sha512, "sha512") -#========================================================= -#eof -#========================================================= +#============================================================================= +# htdigest +#============================================================================= +class htdigest(uh.PasswordHash): + """htdigest hash function. + + .. todo:: + document this hash + """ + name = "htdigest" + setting_kwds = () + context_kwds = ("user", "realm", "encoding") + default_encoding = "utf-8" + + @classmethod + def encrypt(cls, secret, user, realm, encoding=None): + # NOTE: this was deliberately written so that raw bytes are passed through + # unchanged, the encoding kwd is only used to handle unicode values. + if not encoding: + encoding = cls.default_encoding + uh.validate_secret(secret) + if isinstance(secret, unicode): + secret = secret.encode(encoding) + user = to_bytes(user, encoding, "user") + realm = to_bytes(realm, encoding, "realm") + data = render_bytes("%s:%s:%s", user, realm, secret) + return hashlib.md5(data).hexdigest() + + @classmethod + def _norm_hash(cls, hash): + "normalize hash to native string, and validate it" + hash = to_native_str(hash, param="hash") + if len(hash) != 32: + raise uh.exc.MalformedHashError(cls, "wrong size") + for char in hash: + if char not in uh.LC_HEX_CHARS: + raise uh.exc.MalformedHashError(cls, "invalid chars in hash") + return hash + + @classmethod + def verify(cls, secret, hash, user, realm, encoding="utf-8"): + hash = cls._norm_hash(hash) + other = cls.encrypt(secret, user, realm, encoding) + return consteq(hash, other) + + @classmethod + def identify(cls, hash): + try: + cls._norm_hash(hash) + except ValueError: + return False + return True + + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config, user, realm, encoding="utf-8"): + if config is not None: + cls._norm_hash(config) + return cls.encrypt(secret, user, realm, encoding) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/django.py passlib-1.6.1/passlib/handlers/django.py --- passlib-1.5.3/passlib/handlers/django.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/django.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,27 +1,33 @@ """passlib.handlers.django- Django password hash support""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core +from base64 import b64encode from hashlib import md5, sha1 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import h64, handlers as uh, b, bytes, to_unicode, to_hash_str -#pkg -#local +# site +# pkg +from passlib.utils import to_unicode, classproperty +from passlib.utils.compat import b, bytes, str_to_uascii, uascii_to_str, unicode, u +from passlib.utils.pbkdf2 import pbkdf2 +import passlib.utils.handlers as uh +# local __all__ = [ "django_salted_sha1", "django_salted_md5", + "django_bcrypt", + "django_pbkdf2_sha1", + "django_pbkdf2_sha256", "django_des_crypt", "django_disabled", ] -#========================================================= -# lazy imports -#========================================================= +#============================================================================= +# lazy imports & constants +#============================================================================= des_crypt = None def _import_des_crypt(): @@ -30,160 +36,314 @@ from passlib.hash import des_crypt return des_crypt -#========================================================= -#salted hashes -#========================================================= +# django 1.4's salt charset +SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + +#============================================================================= +# salted hashes +#============================================================================= class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler): """base class providing common code for django hashes""" - #must be specified by subclass - along w/ calc_checksum + # name, ident, checksum_size must be set by subclass. + # ident must include "$" suffix. setting_kwds = ("salt", "salt_size") - ident = None #must have "$" suffix - _stub_checksum = None - #common to most subclasses min_salt_size = 0 - default_salt_size = 5 + # NOTE: django 1.0-1.3 would accept empty salt strings. + # django 1.4 won't, but this appears to be regression + # (https://code.djangoproject.com/ticket/18144) + # so presumably it will be fixed in a later release. + default_salt_size = 12 max_salt_size = None - salt_chars = checksum_chars = uh.LC_HEX_CHARS + salt_chars = SALT_CHARS + + checksum_chars = uh.LOWER_HEX_CHARS + + @classproperty + def _stub_checksum(cls): + return cls.checksum_chars[0] * cls.checksum_size @classmethod - def identify(cls, hash): - return uh.identify_prefix(hash, cls.ident) + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) + + def to_string(self): + return uh.render_mc2(self.ident, self.salt, + self.checksum or self._stub_checksum) + +class DjangoVariableHash(uh.HasRounds, DjangoSaltedHash): + """base class providing common code for django hashes w/ variable rounds""" + setting_kwds = DjangoSaltedHash.setting_kwds + ("rounds",) + + min_rounds = 1 @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - ident = cls.ident - assert ident.endswith(u"$") - if not hash.startswith(ident): - raise ValueError("invalid %s hash" % (cls.name,)) - _, salt, chk = hash.split(u"$") - return cls(salt=salt, checksum=chk, strict=True) + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) def to_string(self): - chk = self.checksum or self._stub_checksum - out = u"%s%s$%s" % (self.ident, self.salt, chk) - return to_hash_str(out) + return uh.render_mc3(self.ident, self.rounds, self.salt, + self.checksum or self._stub_checksum) class django_salted_sha1(DjangoSaltedHash): """This class implements Django's Salted SHA1 hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and uses a single round of SHA1. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. - If not specified, a 5 character one will be autogenerated (this is recommended). - If specified, may be any series of characters drawn from the regexp range ``[0-9a-f]``. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + :type salt_size: int :param salt_size: Optional number of characters to use when autogenerating new salts. - Defaults to 5, but can be any non-negative value. + Defaults to 12, but can be any positive value. + + This should be compatible with Django 1.4's :class:`!SHA1PasswordHasher` class. + + .. versionchanged: 1.6 + This class now generates 12-character salts instead of 5, + and generated salts uses the character range ``[0-9a-zA-Z]`` instead of + the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4 + generates these hashes; but hashes generated in this manner will still be + correctly interpreted by earlier versions of Django. """ name = "django_salted_sha1" - ident = u"sha1$" + django_name = "sha1" + ident = u("sha1$") checksum_size = 40 - _stub_checksum = u'0' * 40 - def calc_checksum(self, secret): + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - return to_unicode(sha1(self.salt.encode("ascii") + secret).hexdigest(), "ascii") + return str_to_uascii(sha1(self.salt.encode("ascii") + secret).hexdigest()) class django_salted_md5(DjangoSaltedHash): """This class implements Django's Salted MD5 hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and uses a single round of MD5. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. - If not specified, a 5 character one will be autogenerated (this is recommended). - If specified, may be any series of characters drawn from the regexp range ``[0-9a-f]``. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + :type salt_size: int :param salt_size: Optional number of characters to use when autogenerating new salts. - Defaults to 5, but can be any non-negative value. + Defaults to 12, but can be any positive value. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!MD5PasswordHasher` class. + + .. versionchanged: 1.6 + This class now generates 12-character salts instead of 5, + and generated salts uses the character range ``[0-9a-zA-Z]`` instead of + the ``[0-9a-f]``. This is to be compatible with how Django >= 1.4 + generates these hashes; but hashes generated in this manner will still be + correctly interpreted by earlier versions of Django. """ name = "django_salted_md5" - ident = u"md5$" + django_name = "md5" + ident = u("md5$") checksum_size = 32 - _stub_checksum = u'0' * 32 - def calc_checksum(self, secret): + def _calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(md5(self.salt.encode("ascii") + secret).hexdigest()) + +django_bcrypt = uh.PrefixWrapper("django_bcrypt", "bcrypt", + prefix=u('bcrypt$'), ident=u("bcrypt$"), + # NOTE: this docstring is duplicated in the docs, since sphinx + # seems to be having trouble reading it via autodata:: + doc="""This class implements Django 1.4's BCrypt wrapper, and follows the :ref:`password-hash-api`. + + This is identical to :class:`!bcrypt` itself, but with + the Django-specific prefix ``"bcrypt$"`` prepended. + + See :doc:`/lib/passlib.hash.bcrypt` for more details, + the usage and behavior is identical. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!BCryptPasswordHasher` class. + + .. versionadded:: 1.6 + """) +django_bcrypt.django_name = "bcrypt" + +class django_pbkdf2_sha256(DjangoVariableHash): + """This class implements Django's PBKDF2-HMAC-SHA256 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. + + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha256" + django_name = "pbkdf2_sha256" + ident = u('pbkdf2_sha256$') + min_salt_size = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now + checksum_chars = uh.PADDED_BASE64_CHARS + checksum_size = 44 # 32 bytes -> base64 + default_rounds = 10000 # NOTE: using django default here + _prf = "hmac-sha256" + + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - return to_unicode(md5(self.salt.encode("ascii") + secret).hexdigest(), "ascii") + hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds, + keylen=None, prf=self._prf) + return b64encode(hash).rstrip().decode("ascii") + +class django_pbkdf2_sha1(django_pbkdf2_sha256): + """This class implements Django's PBKDF2-HMAC-SHA1 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: str + :param salt: + Optional salt string. + If not specified, a 12 character one will be autogenerated (this is recommended). + If specified, may be any series of characters drawn from the regexp range ``[0-9a-zA-Z]``. + + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 12, but can be any positive value. -#========================================================= -#other -#========================================================= + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. -class django_des_crypt(DjangoSaltedHash): + This should be compatible with the hashes generated by + Django 1.4's :class:`!PBKDF2SHA1PasswordHasher` class. + + .. versionadded:: 1.6 + """ + name = "django_pbkdf2_sha1" + django_name = "pbkdf2_sha1" + ident = u('pbkdf2_sha1$') + checksum_size = 28 # 20 bytes -> base64 + _prf = "hmac-sha1" + +#============================================================================= +# other +#============================================================================= +class django_des_crypt(uh.HasSalt, uh.GenericHandler): """This class implements Django's :class:`des_crypt` wrapper, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``. - .. note:: - - Django only supports this on Unix systems, - but it is available cross-platform under Passlib. + This should be compatible with the hashes generated by + Django 1.4's :class:`!CryptPasswordHasher` class. + Note that Django only supports this hash on Unix systems + (though :class:`!django_des_crypt` is available cross-platform + under Passlib). + + .. versionchanged:: 1.6 + This class will now accept hashes with empty salt strings, + since Django 1.4 generates them this way. """ - name = "django_des_crypt" - ident = "crypt$" - checksum_chars = salt_chars = uh.H64_CHARS - checksum_size = 13 - min_salt_size = 2 - - # NOTE: checksum is full des_crypt hash, - # including salt as first two digits. - # these should always match first two digits - # of django_des_crypt's salt... - # and all remaining chars of salt are ignored. + django_name = "crypt" + setting_kwds = ("salt", "salt_size") + ident = u("crypt$") + checksum_chars = salt_chars = uh.HASH64_CHARS + checksum_size = 11 + min_salt_size = default_salt_size = 2 + _stub_checksum = u('.')*11 - def __init__(self, **kwds): - super(django_des_crypt, self).__init__(**kwds) + @classmethod + def from_string(cls, hash): + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + if chk: + # chk should be full des_crypt hash + if not salt: + # django 1.4 always uses empty salt field, + # so extract salt from des_crypt hash + salt = chk[:2] + elif salt[:2] != chk[:2]: + # django 1.0 stored 5 chars in salt field, and duplicated + # the first two chars in . we keep the full salt, + # but make sure the first two chars match as sanity check. + raise uh.exc.MalformedHashError(cls, + "first two digits of salt and checksum must match") + # in all cases, strip salt chars from + chk = chk[2:] + return cls(salt=salt, checksum=chk) - # make sure salt embedded in checksum is a match, - # else hash can *never* validate + def to_string(self): + # NOTE: always filling in salt field, so that we're compatible + # with django 1.0 (which requires it) salt = self.salt - chk = self.checksum - if salt and chk and salt[:2] != chk[:2]: - raise ValueError("invalid django_des_crypt hash: " - "first two digits of salt and checksum must match") - - _base_stub_checksum = u'.' * 13 - - @property - def _stub_checksum(self): - "generate stub checksum dynamically, so it matches always matches salt" - stub = self._base_stub_checksum - if self.salt: - return self.salt[:2] + stub[2:] - else: - return stub + chk = salt[:2] + (self.checksum or self._stub_checksum) + return uh.render_mc2(self.ident, salt, chk) - def calc_checksum(self, secret): + def _calc_checksum(self, secret): # NOTE: we lazily import des_crypt, # since most django deploys won't use django_des_crypt global des_crypt if des_crypt is None: _import_des_crypt() - salt = self.salt[:2] - return salt + des_crypt(salt=salt).calc_checksum(secret) + return des_crypt(salt=self.salt[:2])._calc_checksum(secret) class django_disabled(uh.StaticHandler): """This class provides disabled password behavior for Django, and follows the :ref:`password-hash-api`. @@ -199,25 +359,19 @@ @classmethod def identify(cls, hash): - if not hash: - return False - if isinstance(hash, bytes): - return hash == b("!") - else: - return hash == u"!" + hash = uh.to_unicode_for_identify(hash) + return hash == u("!") - @classmethod - def genhash(cls, secret, config): - if secret is None: - raise TypeError("no secret provided") - return to_hash_str(u"!") + def _calc_checksum(self, secret): + return u("!") @classmethod def verify(cls, secret, hash): + uh.validate_secret(secret) if not cls.identify(hash): - raise ValueError("invalid django-disabled hash") + raise uh.exc.InvalidHashError(cls) return False -#========================================================= -#eof -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/fshp.py passlib-1.6.1/passlib/handlers/fshp.py --- passlib-1.5.3/passlib/handlers/fshp.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/fshp.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,32 +1,34 @@ """passlib.handlers.fshp """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from base64 import b64encode, b64decode import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import handlers as uh, bytes, b, to_hash_str +# site +# pkg +from passlib.utils import to_unicode +import passlib.utils.handlers as uh +from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, u,\ + unicode from passlib.utils.pbkdf2 import pbkdf1 -#pkg -#local +# local __all__ = [ 'fshp', ] -#========================================================= -#sha1-crypt -#========================================================= +#============================================================================= +# sha1-crypt +#============================================================================= class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class implements the FSHP password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :param salt: Optional raw salt string. @@ -38,7 +40,7 @@ :param rounds: Optional number of rounds to use. - Defaults to 40000, must be between 1 and 4294967295, inclusive. + Defaults to 50000, must be between 1 and 4294967295, inclusive. :param variant: Optionally specifies variant of FSHP to use. @@ -47,31 +49,45 @@ * ``1`` - uses SHA-2/256 digest (default). * ``2`` - uses SHA-2/384 digest. * ``3`` - uses SHA-2/512 digest. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "fshp" setting_kwds = ("salt", "salt_size", "rounds", "variant") - checksum_chars = uh.PADDED_B64_CHARS + checksum_chars = uh.PADDED_BASE64_CHARS + ident = u("{FSHP") + # checksum_size is property() that depends on variant #--HasRawSalt-- - default_salt_size = 16 #current passlib default, FSHP uses 8 + default_salt_size = 16 # current passlib default, FSHP uses 8 min_salt_size = 0 max_salt_size = None #--HasRounds-- - default_rounds = 16384 #current passlib default, FSHP uses 4096 - min_rounds = 1 #set by FSHP + # FIXME: should probably use different default rounds + # based on the variant. setting for default variant (sha256) for now. + default_rounds = 50000 # current passlib default, FSHP uses 4096 + min_rounds = 1 # set by FSHP max_rounds = 4294967295 # 32-bit integer limit - not set by FSHP rounds_cost = "linear" #--variants-- default_variant = 1 _variant_info = { - #variant: (hash, digest size) + # variant: (hash name, digest size) 0: ("sha1", 20), 1: ("sha256", 32), 2: ("sha384", 48), @@ -79,114 +95,112 @@ } _variant_aliases = dict( [(unicode(k),k) for k in _variant_info] + - [(v[0],k) for k,v in _variant_info.items()] + [(v[0],k) for k,v in iteritems(_variant_info)] ) - #========================================================= - #instance attrs - #========================================================= + #=================================================================== + # instance attrs + #=================================================================== variant = None - #========================================================= - #init - #========================================================= - def __init__(self, variant=None, strict=False, **kwds): - self.variant = self.norm_variant(variant, strict=strict) - super(fshp, self).__init__(strict=strict, **kwds) + #=================================================================== + # init + #=================================================================== + def __init__(self, variant=None, **kwds): + # NOTE: variant must be set first, since it controls checksum size, etc. + self.use_defaults = kwds.get("use_defaults") # load this early + self.variant = self._norm_variant(variant) + super(fshp, self).__init__(**kwds) - @classmethod - def norm_variant(cls, variant, strict=False): + def _norm_variant(self, variant): if variant is None: - if strict: - raise ValueError("no variant specified") - variant = cls.default_variant + if not self.use_defaults: + raise TypeError("no variant specified") + variant = self.default_variant if isinstance(variant, bytes): variant = variant.decode("ascii") if isinstance(variant, unicode): try: - variant = cls._variant_aliases[variant] + variant = self._variant_aliases[variant] except KeyError: raise ValueError("invalid fshp variant") if not isinstance(variant, int): raise TypeError("fshp variant must be int or known alias") - if variant not in cls._variant_info: - raise TypeError("unknown fshp variant") + if variant not in self._variant_info: + raise ValueError("invalid fshp variant") return variant - def norm_checksum(self, checksum, strict=False): - checksum = super(fshp, self).norm_checksum(checksum, strict) - if checksum is not None and len(checksum) != self._variant_info[self.variant][1]: - raise ValueError, "invalid checksum length for FSHP variant" - return checksum - @property - def _info(self): - return self._variant_info[self.variant] - - #========================================================= - #formatting - #========================================================= + def checksum_alg(self): + return self._variant_info[self.variant][0] - @classmethod - def identify(cls, hash): - return uh.identify_prefix(hash, u"{FSHP") + @property + def checksum_size(self): + return self._variant_info[self.variant][1] - _fshp_re = re.compile(ur"^\{FSHP(\d+)\|(\d+)\|(\d+)\}([a-zA-Z0-9+/]+={0,3})$") + #=================================================================== + # formatting + #=================================================================== + + _hash_regex = re.compile(u(r""" + ^ + \{FSHP + (\d+)\| # variant + (\d+)\| # salt size + (\d+)\} # rounds + ([a-zA-Z0-9+/]+={0,3}) # digest + $"""), re.X) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._fshp_re.match(hash) + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) if not m: - raise ValueError("not a valid FSHP hash") + raise uh.exc.InvalidHashError(cls) variant, salt_size, rounds, data = m.group(1,2,3,4) variant = int(variant) salt_size = int(salt_size) rounds = int(rounds) try: data = b64decode(data.encode("ascii")) - except ValueError: - raise ValueError("malformed FSHP hash") + except TypeError: + raise uh.exc.MalformedHashError(cls) salt = data[:salt_size] chk = data[salt_size:] - return cls(checksum=chk, salt=salt, rounds=rounds, - variant=variant, strict=True) + return cls(salt=salt, checksum=chk, rounds=rounds, variant=variant) + + @property + def _stub_checksum(self): + return b('\x00') * self.checksum_size def to_string(self): - chk = self.checksum - if not chk: #fill in stub checksum - chk = b('\x00') * self._info[1] + chk = self.checksum or self._stub_checksum salt = self.salt - data = b64encode(salt+chk).decode("ascii") - hash = u"{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data) - return to_hash_str(hash) - - #========================================================= - #backend - #========================================================= + data = bascii_to_str(b64encode(salt+chk)) + return "{FSHP%d|%d|%d}%s" % (self.variant, len(salt), self.rounds, data) + + #=================================================================== + # backend + #=================================================================== - def calc_checksum(self, secret): - hash, klen = self._info + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - #NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed. - # this has only a minimal impact on security, - # but it is worth noting this deviation. + # NOTE: for some reason, FSHP uses pbkdf1 with password & salt reversed. + # this has only a minimal impact on security, + # but it is worth noting this deviation. return pbkdf1( secret=self.salt, salt=secret, rounds=self.rounds, - keylen=klen, - hash=hash, + keylen=self.checksum_size, + hash=self.checksum_alg, ) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/ldap_digests.py passlib-1.6.1/passlib/handlers/ldap_digests.py --- passlib-1.5.3/passlib/handlers/ldap_digests.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/ldap_digests.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,19 +1,22 @@ """passlib.handlers.digests - plain hash digests """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from base64 import b64encode, b64decode from hashlib import md5, sha1 import logging; log = logging.getLogger(__name__) import re from warnings import warn -#site -#libs -from passlib.utils import handlers as uh, unix_crypt_schemes, b, bytes, to_hash_str -#pkg -#local +# site +# pkg +from passlib.handlers.misc import plaintext +from passlib.utils import to_native_str, unix_crypt_schemes, \ + classproperty, to_unicode +from passlib.utils.compat import b, bytes, uascii_to_str, unicode, u +import passlib.utils.handlers as uh +# local __all__ = [ "ldap_plaintext", "ldap_md5", @@ -31,198 +34,221 @@ "ldap_sha512_crypt", ] -#========================================================= -#ldap helpers -#========================================================= -#reference - http://www.openldap.org/doc/admin24/security.html - +#============================================================================= +# ldap helpers +#============================================================================= class _Base64DigestHelper(uh.StaticHandler): "helper for ldap_md5 / ldap_sha1" - #XXX: could combine this with hex digests in digests.py - - ident = None #required - prefix identifier - _hash_func = None #required - hash function - _pat = None #required - regexp to recognize hash - checksum_chars = uh.PADDED_B64_CHARS + # XXX: could combine this with hex digests in digests.py - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + ident = None # required - prefix identifier + _hash_func = None # required - hash function + _hash_regex = None # required - regexp to recognize hash + checksum_chars = uh.PADDED_BASE64_CHARS + + @classproperty + def _hash_prefix(cls): + "tell StaticHandler to strip ident from checksum" + return cls.ident - @classmethod - def genhash(cls, secret, hash): - if secret is None: - raise TypeError("no secret provided") + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - if hash is not None and not cls.identify(hash): - raise ValueError("not a %s hash" % (cls.name,)) - chk = cls._hash_func(secret).digest() - hash = cls.ident + b64encode(chk).decode("ascii") - return to_hash_str(hash) + chk = self._hash_func(secret).digest() + return b64encode(chk).decode("ascii") class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): "helper for ldap_salted_md5 / ldap_salted_sha1" - setting_kwds = ("salt",) - checksum_chars = uh.PADDED_B64_CHARS + setting_kwds = ("salt", "salt_size") + checksum_chars = uh.PADDED_BASE64_CHARS - ident = None #required - prefix identifier - _hash_func = None #required - hash function - _pat = None #required - regexp to recognize hash - _stub_checksum = None #required - default checksum to plug in + ident = None # required - prefix identifier + checksum_size = None # required + _hash_func = None # required - hash function + _hash_regex = None # required - regexp to recognize hash + _stub_checksum = None # required - default checksum to plug in min_salt_size = max_salt_size = 4 - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + # NOTE: openldap implementation uses 4 byte salt, + # but it's been reported (issue 30) that some servers use larger salts. + # the semi-related rfc3112 recommends support for up to 16 byte salts. + min_salt_size = 4 + default_salt_size = 4 + max_salt_size = 16 @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode('ascii') - m = cls._pat.match(hash) + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) if not m: - raise ValueError("not a %s hash" % (cls.name,)) - data = b64decode(m.group("tmp").encode("ascii")) - chk, salt = data[:-4], data[-4:] - return cls(checksum=chk, salt=salt, strict=True) + raise uh.exc.InvalidHashError(cls) + try: + data = b64decode(m.group("tmp").encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + cs = cls.checksum_size + assert cs + return cls(checksum=data[:cs], salt=data[cs:]) def to_string(self): data = (self.checksum or self._stub_checksum) + self.salt hash = self.ident + b64encode(data).decode("ascii") - return to_hash_str(hash) + return uascii_to_str(hash) - def calc_checksum(self, secret): - if secret is None: - raise TypeError("no secret provided") + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") return self._hash_func(secret + self.salt).digest() -#========================================================= -#implementations -#========================================================= +#============================================================================= +# implementations +#============================================================================= class ldap_md5(_Base64DigestHelper): """This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`. - The :meth:`encrypt()` and :meth:`genconfig` methods have no optional keywords. + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. """ name = "ldap_md5" - setting_kwds = () - - ident = u"{MD5}" + ident = u("{MD5}") _hash_func = md5 - _pat = re.compile(ur"^\{MD5\}(?P[+/a-zA-Z0-9]{22}==)$") + _hash_regex = re.compile(u(r"^\{MD5\}(?P[+/a-zA-Z0-9]{22}==)$")) class ldap_sha1(_Base64DigestHelper): """This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`. - The :meth:`encrypt()` and :meth:`genconfig` methods have no optional keywords. + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. """ name = "ldap_sha1" - setting_kwds = () - - ident = u"{SHA}" + ident = u("{SHA}") _hash_func = sha1 - _pat = re.compile(ur"^\{SHA\}(?P[+/a-zA-Z0-9]{27}=)$") + _hash_regex = re.compile(u(r"^\{SHA\}(?P[+/a-zA-Z0-9]{27}=)$")) class ldap_salted_md5(_SaltedBase64DigestHelper): """This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`. - It supports a 4-byte salt. + It supports a 4-16 byte salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keyword: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword: + :type salt: bytes :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). - If specified, it must be a 4 byte string; each byte may have any value from 0x00 .. 0xff. + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 4 bytes for compatibility with the LDAP spec, + but some systems use larger salts, and Passlib supports + any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This format now supports variable length salts, instead of a fix 4 bytes. """ name = "ldap_salted_md5" - ident = u"{SMD5}" + ident = u("{SMD5}") + checksum_size = 16 _hash_func = md5 - _pat = re.compile(ur"^\{SMD5\}(?P[+/a-zA-Z0-9]{27}=)$") + _hash_regex = re.compile(u(r"^\{SMD5\}(?P[+/a-zA-Z0-9]{27,}={0,2})$")) _stub_checksum = b('\x00') * 16 class ldap_salted_sha1(_SaltedBase64DigestHelper): """This class stores passwords using LDAP's salted SHA1 format, and follows the :ref:`password-hash-api`. - It supports a 4-byte salt. + It supports a 4-16 byte salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keyword: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword: + :type salt: bytes :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). - If specified, it must be a 4 byte string; each byte may have any value from 0x00 .. 0xff. + If specified, it may be any 4-16 byte string. + + :type salt_size: int + :param salt_size: + Optional number of bytes to use when autogenerating new salts. + Defaults to 4 bytes for compatibility with the LDAP spec, + but some systems use larger salts, and Passlib supports + any value between 4-16. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + .. versionchanged:: 1.6 + This format now supports variable length salts, instead of a fix 4 bytes. """ name = "ldap_salted_sha1" - ident = u"{SSHA}" + ident = u("{SSHA}") + checksum_size = 20 _hash_func = sha1 - _pat = re.compile(ur"^\{SSHA\}(?P[+/a-zA-Z0-9]{32})$") + _hash_regex = re.compile(u(r"^\{SSHA\}(?P[+/a-zA-Z0-9]{32,}={0,2})$")) _stub_checksum = b('\x00') * 20 -class ldap_plaintext(uh.StaticHandler): +class ldap_plaintext(plaintext): """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. This class acts much like the generic :class:`!passlib.hash.plaintext` handler, except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix used by RFC2307 passwords. - Unicode passwords will be encoded using utf-8. - """ - name = "ldap_plaintext" + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keyword: - _2307_pat = re.compile(ur"^\{\w+\}.*$") + :type encoding: str + :param encoding: + This controls the character encoding to use (defaults to ``utf-8``). - @classmethod - def identify(cls, hash): - if not hash: - return False - if isinstance(hash, bytes): - try: - hash = hash.decode("utf-8") - except UnicodeDecodeError: - return False - #NOTE: identifies all strings EXCEPT those which match... - return cls._2307_pat.match(hash) is None + This encoding will be used to encode :class:`!unicode` passwords + under Python 2, and decode :class:`!bytes` hashes under Python 3. - @classmethod - def genhash(cls, secret, hash): - if hash is not None and not cls.identify(hash): - raise ValueError("not a valid ldap_plaintext hash") - if secret is None: - raise TypeError("secret must be string") - return to_hash_str(secret, "utf-8") + .. versionchanged:: 1.6 + The ``encoding`` keyword was added. + """ + # NOTE: this subclasses plaintext, since all it does differently + # is override identify() + + name = "ldap_plaintext" + _2307_pat = re.compile(u(r"^\{\w+\}.*$")) @classmethod - def _norm_hash(cls, hash): - if isinstance(hash, bytes): - #XXX: current code uses utf-8 - # if existing hashes use something else, - # probably have to modify this code to allow hash_encoding - # to be specified as an option. - hash = hash.decode("utf-8") - return hash - -#========================================================= -#{CRYPT} wrappers -#========================================================= + def identify(cls, hash): + # NOTE: identifies all strings EXCEPT those with {XXX} prefix + hash = uh.to_unicode_for_identify(hash) + return bool(hash) and cls._2307_pat.match(hash) is None +#============================================================================= +# {CRYPT} wrappers # the following are wrappers around the base crypt algorithms, # which add the ldap required {CRYPT} prefix - +#============================================================================= ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ] def _init_ldap_crypt_handlers(): - #XXX: it's not nice to play in globals like this, - # but don't want to write all all these handlers + # NOTE: I don't like to implicitly modify globals() like this, + # but don't want to write out all these handlers out either :) g = globals() for wname in unix_crypt_schemes: name = 'ldap_' + wname - g[name] = uh.PrefixWrapper(name, wname, prefix=u"{CRYPT}", lazy=True) + g[name] = uh.PrefixWrapper(name, wname, prefix=u("{CRYPT}"), lazy=True) del g _init_ldap_crypt_handlers() @@ -231,7 +257,7 @@ ## global _lcn_host ## if _lcn_host is None: ## from passlib.hosts import host_context -## schemes = host_context.policy.schemes() +## schemes = host_context.schemes() ## _lcn_host = [ ## "ldap_" + name ## for name in unix_crypt_names @@ -239,6 +265,6 @@ ## ] ## return _lcn_host -#========================================================= -#eof -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/md5_crypt.py passlib-1.6.1/passlib/handlers/md5_crypt.py --- passlib-1.5.3/passlib/handlers/md5_crypt.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/md5_crypt.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,227 +1,269 @@ """passlib.handlers.md5_crypt - md5-crypt algorithm""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import b, bytes, to_bytes, h64, safe_os_crypt, \ - classproperty, handlers as uh -#pkg -#local +# site +# pkg +from passlib.utils import classproperty, h64, safe_crypt, test_crypt, repeat_string +from passlib.utils.compat import b, bytes, irange, unicode, u +import passlib.utils.handlers as uh +# local __all__ = [ "md5_crypt", "apr_md5_crypt", ] -#========================================================= -#pure-python backend -#========================================================= -B_NULL = b("\x00") -B_MD5_MAGIC = b("$1$") -B_APR_MAGIC = b("$apr1$") +#============================================================================= +# pure-python backend +#============================================================================= +_BNULL = b("\x00") +_MD5_MAGIC = b("$1$") +_APR_MAGIC = b("$apr1$") + +# pre-calculated offsets used to speed up C digest stage (see notes below). +# sequence generated using the following: + ##perms_order = "p,pp,ps,psp,sp,spp".split(",") + ##def offset(i): + ## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") + + ## ("p" if i % 7 else "") + ("" if i % 2 else "p")) + ## return perms_order.index(key) + ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)] +_c_digest_offsets = ( + (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3), + (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1), + (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3), + ) -def raw_md5_crypt(secret, salt, apr=False): - """perform raw md5-crypt calculation - - :arg secret: - password, bytes or unicode (encoded to utf-8) +# map used to transpose bytes when encoding final digest +_transpose_map = (12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11) - :arg salt: - salt portion of hash, bytes or unicode (encoded to ascii), - clipped to max 8 bytes. +def _raw_md5_crypt(pwd, salt, use_apr=False): + """perform raw md5-crypt calculation - :param apr: - flag to use apache variant + this function provides a pure-python implementation of the internals + for the MD5-Crypt algorithms; it doesn't handle any of the + parsing/validation of the hash strings themselves. + + :arg pwd: password chars/bytes to encrypt + :arg salt: salt chars to use + :arg use_apr: use apache variant :returns: - encoded checksum as unicode + encoded checksum chars """ - #NOTE: regarding 'apr' format: + # NOTE: regarding 'apr' format: # really, apache? you had to invent a whole new "$apr1$" format, # when all you did was change the ident incorporated into the hash? # would love to find webpage explaining why just using a portable # implementation of $1$ wasn't sufficient. *nothing* else was changed. - #validate secret - #FIXME: can't find definitive policy on how md5-crypt handles non-ascii. - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - - #validate salt - if isinstance(salt, unicode): - salt = salt.encode("ascii") - if len(salt) > 8: - salt = salt[:8] - - #primary hash = secret+id+salt+... - h = md5(secret) - h.update(B_APR_MAGIC if apr else B_MD5_MAGIC) - h.update(salt) - - # primary hash - add len(secret) chars of tmp hash, - # where temp hash is md5(secret+salt+secret) - tmp = md5(secret + salt + secret).digest() - assert len(tmp) == 16 - slen = len(secret) - h.update(tmp * (slen//16) + tmp[:slen % 16]) + #=================================================================== + # init & validate inputs + #=================================================================== + + # validate secret + # XXX: not sure what official unicode policy is, using this as default + if isinstance(pwd, unicode): + pwd = pwd.encode("utf-8") + assert isinstance(pwd, bytes), "pwd not unicode or bytes" + if _BNULL in pwd: + raise uh.exc.NullPasswordError(md5_crypt) + pwd_len = len(pwd) + + # validate salt - should have been taken care of by caller + assert isinstance(salt, unicode), "salt not unicode" + salt = salt.encode("ascii") + assert len(salt) < 9, "salt too large" + # NOTE: spec says salts larger than 8 bytes should be truncated, + # instead of causing an error. this function assumes that's been + # taken care of by the handler class. + + # load APR specific constants + if use_apr: + magic = _APR_MAGIC + else: + magic = _MD5_MAGIC + + #=================================================================== + # digest B - used as subinput to digest A + #=================================================================== + db = md5(pwd + salt + pwd).digest() + + #=================================================================== + # digest A - used to initialize first round of digest C + #=================================================================== + # start out with pwd + magic + salt + a_ctx = md5(pwd + magic + salt) + a_ctx_update = a_ctx.update + + # add pwd_len bytes of b, repeating b as many times as needed. + a_ctx_update(repeat_string(db, pwd_len)) + + # add null chars & first char of password + # NOTE: this may have historically been a bug, + # where they meant to use db[0] instead of B_NULL, + # but the original code memclear'ed db, + # and now all implementations have to use this. + i = pwd_len + evenchar = pwd[:1] + while i: + a_ctx_update(_BNULL if i & 1 else evenchar) + i >>= 1 + + # finish A + da = a_ctx.digest() + + #=================================================================== + # digest C - for a 1000 rounds, combine A, S, and P + # digests in various ways; in order to burn CPU time. + #=================================================================== - # primary hash - add null chars & first char of secret !?! + # NOTE: the original MD5-Crypt implementation performs the C digest + # calculation using the following loop: # - # this may have historically been a bug, - # where they meant to use tmp[0] instead of '\x00', - # but the code memclear'ed the buffer, - # and now all implementations have to use this. + ##dc = da + ##i = 0 + ##while i < rounds: + ## tmp_ctx = md5(pwd if i & 1 else dc) + ## if i % 3: + ## tmp_ctx.update(salt) + ## if i % 7: + ## tmp_ctx.update(pwd) + ## tmp_ctx.update(dc if i & 1 else pwd) + ## dc = tmp_ctx.digest() + ## i += 1 # - # sha-crypt replaced this step with - # something more useful, anyways - idx = len(secret) - evenchar = secret[:1] - while idx > 0: - h.update(B_NULL if idx & 1 else evenchar) - idx >>= 1 - result = h.digest() - - #next: - # do 1000 rounds of md5 to make things harder. - # each round we do digest of round-specific content, - # where content is formed from concatenation of... - # secret if round % 2 else result - # salt if round % 3 - # secret if round % 7 - # result if round % 2 else secret + # The code Passlib uses (below) implements an equivalent algorithm, + # it's just been heavily optimized to pre-calculate a large number + # of things beforehand. It works off of a couple of observations + # about the original algorithm: # - #NOTE: - # instead of doing this directly, this implementation - # pre-computes all the combinations of strings & md5 hash objects - # that will be needed, in order to perform round operations as fast as possible - # (so that each round consists of one hash create/copy + 1 update + 1 digest) + # 1. each round is a combination of 'dc', 'salt', and 'pwd'; determined + # by the whether 'i' a multiple of 2,3, and/or 7. + # 2. since lcm(2,3,7)==42, the series of combinations will repeat + # every 42 rounds. + # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)'; + # while odd rounds 1-41 consist of hash(round-specific-constant + dc) # - #TODO: might be able to optimize even further by removing need for tests, since - # if/then pattern is easily predicatble - - # pattern is 7-0-1-0-3-0 (where 1 bit = mult 2, 2 bit = mult 3, 3 bit = mult 7) - secret_secret = secret*2 - salt_secret = salt+secret - salt_secret_secret = salt + secret*2 - secret_hash = md5(secret).copy - secret_secret_hash = md5(secret_secret).copy - secret_salt_hash = md5(secret+salt).copy - secret_salt_secret_hash = md5(secret+salt_secret).copy - for idx in xrange(1000): - if idx & 1: - if idx % 3: - if idx % 7: - h = secret_salt_secret_hash() - else: - h = secret_salt_hash() - elif idx % 7: - h = secret_secret_hash() - else: - h = secret_hash() - h.update(result) - else: - h = md5(result) - if idx % 3: - if idx % 7: - h.update(salt_secret_secret) - else: - h.update(salt_secret) - elif idx % 7: - h.update(secret_secret) - else: - h.update(secret) - result = h.digest() - - #encode resulting hash - return h64.encode_transposed_bytes(result, _chk_offsets).decode("ascii") - -_chk_offsets = ( - 12,6,0, - 13,7,1, - 14,8,2, - 15,9,3, - 5,10,4, - 11, -) - -#========================================================= -#handler -#========================================================= -class _Md5Common(uh.HasSalt, uh.GenericHandler): + # Using these observations, the following code... + # * calculates the round-specific combination of salt & pwd for each round 0-41 + # * runs through as many 42-round blocks as possible + # * runs through as many pairs of rounds as possible for remaining rounds + # * performs once last round if the total rounds should be odd. + # + # this cuts out a lot of the control overhead incurred when running the + # original loop 40,000+ times in python, resulting in ~20% increase in + # speed under CPython (though still 2x slower than glibc crypt) + + # prepare the 6 combinations of pwd & salt which are needed + # (order of 'perms' must match how _c_digest_offsets was generated) + pwd_pwd = pwd+pwd + pwd_salt = pwd+salt + perms = [pwd, pwd_pwd, pwd_salt, pwd_salt+pwd, salt+pwd, salt+pwd_pwd] + + # build up list of even-round & odd-round constants, + # and store in 21-element list as (even,odd) pairs. + data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets] + + # perform 23 blocks of 42 rounds each (for a total of 966 rounds) + dc = da + blocks = 23 + while blocks: + for even, odd in data: + dc = md5(odd + md5(dc + even).digest()).digest() + blocks -= 1 + + # perform 17 more pairs of rounds (34 more rounds, for a total of 1000) + for even, odd in data[:17]: + dc = md5(odd + md5(dc + even).digest()).digest() + + #=================================================================== + # encode digest using appropriate transpose map + #=================================================================== + return h64.encode_transposed_bytes(dc, _transpose_map).decode("ascii") + +#============================================================================= +# handler +#============================================================================= +class _MD5_Common(uh.HasSalt, uh.GenericHandler): "common code for md5_crypt and apr_md5_crypt" - #========================================================= - #algorithm information - #========================================================= - #--GenericHandler-- - #name in subclass + #=================================================================== + # class attrs + #=================================================================== + # name - set in subclass setting_kwds = ("salt", "salt_size") - #ident in subclass + # ident - set in subclass checksum_size = 22 - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS - #--HasSalt-- min_salt_size = 0 max_salt_size = 8 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - #========================================================= - #internal helpers - #========================================================= + #=================================================================== + # methods + #=================================================================== @classmethod def from_string(cls, hash): - salt, chk = uh.parse_mc2(hash, cls.ident, cls.name) - return cls(salt=salt, checksum=chk, strict=bool(chk)) + salt, chk = uh.parse_mc2(hash, cls.ident, handler=cls) + return cls(salt=salt, checksum=chk) def to_string(self): return uh.render_mc2(self.ident, self.salt, self.checksum) - #========================================================= - #primary interface - #========================================================= - #calc_checksum in subclass - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#handler -#========================================================= -class md5_crypt(uh.HasManyBackends, _Md5Common): + # _calc_checksum() - provided by subclass + + #=================================================================== + # eoc + #=================================================================== + +class md5_crypt(uh.HasManyBackends, _MD5_Common): """This class implements the MD5-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. - It will use the first available of two possible backends: - - * stdlib :func:`crypt()`, if the host OS supports MD5-Crypt. - * a pure python implementation of MD5-Crypt built into passlib. + :type salt_size: int + :param salt_size: + Optional number of characters to use when autogenerating new salts. + Defaults to 8, but can be any value between 0 and 8. + (This is mainly needed when generating Cisco-compatible hashes, + which require ``salt_size=4``). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. - You can see which backend is in use by calling the :meth:`get_backend()` method. + .. versionadded:: 1.6 """ - #========================================================= - #algorithm information - #========================================================= + #=================================================================== + # class attrs + #=================================================================== name = "md5_crypt" - ident = u"$1$" + ident = u("$1$") - #========================================================= - #primary interface - #========================================================= - #FIXME: can't find definitive policy on how md5-crypt handles non-ascii. - # all backends currently coerce -> utf-8 + #=================================================================== + # methods + #=================================================================== + # FIXME: can't find definitive policy on how md5-crypt handles non-ascii. + # all backends currently coerce -> utf-8 backends = ("os_crypt", "builtin") @@ -229,54 +271,63 @@ @classproperty def _has_backend_os_crypt(cls): - h = u'$1$test$pi/xDtU5WFVRqYS6BMU8X/' - return bool(safe_os_crypt and safe_os_crypt(u"test",h)[1]==h) + return test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/') def _calc_checksum_builtin(self, secret): - return raw_md5_crypt(secret, self.salt) + return _raw_md5_crypt(secret, self.salt) def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.ident + self.salt) - if ok: + config = self.ident + self.salt + hash = safe_crypt(secret, config) + if hash: + assert hash.startswith(config) and len(hash) == len(config) + 23 return hash[-22:] else: return self._calc_checksum_builtin(secret) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#apache variant of md5-crypt -#========================================================= -class apr_md5_crypt(_Md5Common): + #=================================================================== + # eoc + #=================================================================== + +class apr_md5_crypt(_MD5_Common): """This class implements the Apr-MD5-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 0-8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #algorithm information - #========================================================= + #=================================================================== + # class attrs + #=================================================================== name = "apr_md5_crypt" - ident = u"$apr1$" + ident = u("$apr1$") - #========================================================= - #primary interface - #========================================================= - def calc_checksum(self, secret): - return raw_md5_crypt(secret, self.salt, apr=True) - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # methods + #=================================================================== + def _calc_checksum(self, secret): + return _raw_md5_crypt(secret, self.salt, use_apr=True) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/misc.py passlib-1.6.1/passlib/handlers/misc.py --- passlib-1.5.3/passlib/handlers/misc.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/misc.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,24 +1,27 @@ """passlib.handlers.misc - misc generic handlers """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core +import sys import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import to_hash_str, handlers as uh, bytes -#pkg -#local +# site +# pkg +from passlib.utils import to_native_str, consteq +from passlib.utils.compat import bytes, unicode, u, b, base_string_types +import passlib.utils.handlers as uh +# local __all__ = [ + "unix_disabled", "unix_fallback", "plaintext", ] -#========================================================= -#handler -#========================================================= +#============================================================================= +# handler +#============================================================================= class unix_fallback(uh.StaticHandler): """This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`. @@ -32,56 +35,208 @@ * by default it rejects all passwords if the hash is an empty string, but if ``enable_wildcard=True`` is passed to verify(), all passwords will be allowed through if the hash is an empty string. + + .. deprecated:: 1.6 + This has been deprecated due to it's "wildcard" feature, + and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead. """ name = "unix_fallback" context_kwds = ("enable_wildcard",) - _stub_config = "!" @classmethod def identify(cls, hash): - return hash is not None + if isinstance(hash, base_string_types): + return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + + def __init__(self, enable_wildcard=False, **kwds): + warn("'unix_fallback' is deprecated, " + "and will be removed in Passlib 1.8; " + "please use 'unix_disabled' instead.", + DeprecationWarning) + super(unix_fallback, self).__init__(**kwds) + self.enable_wildcard = enable_wildcard + + @classmethod + def genhash(cls, secret, config): + # override default to preserve checksum + if config is None: + return cls.encrypt(secret) + else: + uh.validate_secret(secret) + self = cls.from_string(config) + self.checksum = self._calc_checksum(secret) + return self.to_string() + + def _calc_checksum(self, secret): + if self.checksum: + # NOTE: hash will generally be "!", but we want to preserve + # it in case it's something else, like "*". + return self.checksum + else: + return u("!") @classmethod - def genhash(cls, secret, hash, enable_wildcard=False): - if secret is None: - raise TypeError("secret must be string") - if hash is None: - raise ValueError("no hash provided") - return to_hash_str(hash) + def verify(cls, secret, hash, enable_wildcard=False): + uh.validate_secret(secret) + if not isinstance(hash, base_string_types): + raise uh.exc.ExpectedStringError(hash, "hash") + elif hash: + return False + else: + return enable_wildcard + +_MARKER_CHARS = u("*!") +_MARKER_BYTES = b("*!") + +class unix_disabled(uh.PasswordHash): + """This class provides disabled password behavior for unix shadow files, + and follows the :ref:`password-hash-api`. + + This class does not implement a hash, but instead matches the "disabled account" + strings found in ``/etc/shadow`` on most Unix variants. "encrypting" a password + will simply return the disabled account marker. It will reject all passwords, + no matter the hash string. The :meth:`~passlib.ifc.PasswordHash.encrypt` + method supports one optional keyword: + + :type marker: str + :param marker: + Optional marker string which overrides the platform default + used to indicate a disabled account. + + If not specified, this will default to ``"*"`` on BSD systems, + and use the Linux default ``"!"`` for all other platforms. + (:attr:`!unix_disabled.default_marker` will contain the default value) + + .. versionadded:: 1.6 + This class was added as a replacement for the now-deprecated + :class:`unix_fallback` class, which had some undesirable features. + """ + name = "unix_disabled" + setting_kwds = ("marker",) + context_kwds = () + + if 'bsd' in sys.platform: # pragma: no cover -- runtime detection + default_marker = u("*") + else: + # use the linux default for other systems + # (glibc also supports adding old hash after the marker + # so it can be restored later). + default_marker = u("!") @classmethod - def verify(cls, secret, hash, enable_wildcard=False): - if hash is None: - raise ValueError("no hash provided") - return enable_wildcard and not hash + def identify(cls, hash): + # NOTE: technically, anything in the /etc/shadow password field + # which isn't valid crypt() output counts as "disabled". + # but that's rather ambiguous, and it's hard to predict what + # valid output is for unknown crypt() implementations. + # so to be on the safe side, we only match things *known* + # to be disabled field indicators, and will add others + # as they are found. things beginning w/ "$" should *never* match. + # + # things currently matched: + # * linux uses "!" + # * bsd uses "*" + # * linux may use "!" + hash to disable but preserve original hash + # * linux counts empty string as "any password" + if isinstance(hash, unicode): + start = _MARKER_CHARS + elif isinstance(hash, bytes): + start = _MARKER_BYTES + else: + raise uh.exc.ExpectedStringError(hash, "hash") + return not hash or hash[0] in start + + @classmethod + def encrypt(cls, secret, marker=None): + return cls.genhash(secret, None, marker) + + @classmethod + def verify(cls, secret, hash): + uh.validate_secret(secret) + if not cls.identify(hash): # handles typecheck + raise uh.exc.InvalidHashError(cls) + return False + + @classmethod + def genconfig(cls): + return None + + @classmethod + def genhash(cls, secret, config, marker=None): + uh.validate_secret(secret) + if config is not None and not cls.identify(config): # handles typecheck + raise uh.exc.InvalidHashError(cls) + if config: + # we want to preserve the existing str, + # since it might contain a disabled password hash ("!" + hash) + return to_native_str(config, param="config") + # if None or empty string, replace with marker + if marker: + if not cls.identify(marker): + raise ValueError("invalid marker: %r" % marker) + else: + marker = cls.default_marker + assert marker and cls.identify(marker) + return to_native_str(marker, param="marker") -class plaintext(uh.StaticHandler): +class plaintext(uh.PasswordHash): """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. - Unicode passwords will be encoded using utf-8. + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the + following additional contextual keyword: + + :type encoding: str + :param encoding: + This controls the character encoding to use (defaults to ``utf-8``). + + This encoding will be used to encode :class:`!unicode` passwords + under Python 2, and decode :class:`!bytes` hashes under Python 3. + + .. versionchanged:: 1.6 + The ``encoding`` keyword was added. """ + # NOTE: this is subclassed by ldap_plaintext + name = "plaintext" + setting_kwds = () + context_kwds = ("encoding",) + default_encoding = "utf-8" @classmethod def identify(cls, hash): - return hash is not None + if isinstance(hash, base_string_types): + return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + + @classmethod + def encrypt(cls, secret, encoding=None): + uh.validate_secret(secret) + if not encoding: + encoding = cls.default_encoding + return to_native_str(secret, encoding, "secret") + + @classmethod + def verify(cls, secret, hash, encoding=None): + if not encoding: + encoding = cls.default_encoding + hash = to_native_str(hash, encoding, "hash") + if not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return consteq(cls.encrypt(secret, encoding), hash) + + @classmethod + def genconfig(cls): + return None @classmethod - def genhash(cls, secret, hash): - if secret is None: - raise TypeError("secret must be string") - return to_hash_str(secret, "utf-8") - - @classmethod - def _norm_hash(cls, hash): - if isinstance(hash, bytes): - #XXX: current code uses utf-8 - # if existing hashes use something else, - # probably have to modify this code to allow hash_encoding - # to be specified as an option. - hash = hash.decode("utf-8") - return hash - -#========================================================= -#eof -#========================================================= + def genhash(cls, secret, hash, encoding=None): + if hash is not None and not cls.identify(hash): + raise uh.exc.InvalidHashError(cls) + return cls.encrypt(secret, encoding) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/mssql.py passlib-1.6.1/passlib/handlers/mssql.py --- passlib-1.5.3/passlib/handlers/mssql.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/mssql.py 2012-08-01 17:10:03.000000000 +0000 @@ -0,0 +1,246 @@ +"""passlib.handlers.mssql - MS-SQL Password Hash + +Notes +===== +MS-SQL has used a number of hash algs over the years, +most of which were exposed through the undocumented +'pwdencrypt' and 'pwdcompare' sql functions. + +Known formats +------------- +6.5 + snefru hash, ascii encoded password + no examples found + +7.0 + snefru hash, unicode (what encoding?) + saw ref that these blobs were 16 bytes in size + no examples found + +2000 + byte string using displayed as 0x hex, using 0x0100 prefix. + contains hashes of password and upper-case password. + +2007 + same as 2000, but without the upper-case hash. + +refs +---------- +https://blogs.msdn.com/b/lcris/archive/2007/04/30/sql-server-2005-about-login-password-hashes.aspx?Redirected=true +http://us.generation-nt.com/securing-passwords-hash-help-35429432.html +http://forum.md5decrypter.co.uk/topic230-mysql-and-mssql-get-password-hashes.aspx +http://www.theregister.co.uk/2002/07/08/cracking_ms_sql_server_passwords/ +""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify, unhexlify +from hashlib import sha1 +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import consteq +from passlib.utils.compat import b, bytes, bascii_to_str, unicode, u +import passlib.utils.handlers as uh +# local +__all__ = [ + "mssql2000", + "mssql2005", +] + +#============================================================================= +# mssql 2000 +#============================================================================= +def _raw_mssql(secret, salt): + assert isinstance(secret, unicode) + assert isinstance(salt, bytes) + return sha1(secret.encode("utf-16-le") + salt).digest() + +BIDENT = b("0x0100") +##BIDENT2 = b("\x01\x00") +UIDENT = u("0x0100") + +def _ident_mssql(hash, csize, bsize): + "common identify for mssql 2000/2005" + if isinstance(hash, unicode): + if len(hash) == csize and hash.startswith(UIDENT): + return True + elif isinstance(hash, bytes): + if len(hash) == csize and hash.startswith(BIDENT): + return True + ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes + ## return True + else: + raise uh.exc.ExpectedStringError(hash, "hash") + return False + +def _parse_mssql(hash, csize, bsize, handler): + "common parser for mssql 2000/2005; returns 4 byte salt + checksum" + if isinstance(hash, unicode): + if len(hash) == csize and hash.startswith(UIDENT): + try: + return unhexlify(hash[6:].encode("utf-8")) + except TypeError: # throw when bad char found + pass + elif isinstance(hash, bytes): + # assumes ascii-compat encoding + assert isinstance(hash, bytes) + if len(hash) == csize and hash.startswith(BIDENT): + try: + return unhexlify(hash[6:]) + except TypeError: # throw when bad char found + pass + ##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes + ## return hash[2:] + else: + raise uh.exc.ExpectedStringError(hash, "hash") + raise uh.exc.InvalidHashError(handler) + +class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the password hash used by MS-SQL 2000, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 bytes in length. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "mssql2000" + setting_kwds = ("salt",) + checksum_size = 40 + min_salt_size = max_salt_size = 4 + _stub_checksum = b("\x00") * 40 + + #=================================================================== + # formatting + #=================================================================== + + # 0100 - 2 byte identifier + # 4 byte salt + # 20 byte checksum + # 20 byte checksum + # = 46 bytes + # encoded '0x' + 92 chars = 94 + + @classmethod + def identify(cls, hash): + return _ident_mssql(hash, 94, 46) + + @classmethod + def from_string(cls, hash): + data = _parse_mssql(hash, 94, 46, cls) + return cls(salt=data[:4], checksum=data[4:]) + + def to_string(self): + raw = self.salt + (self.checksum or self._stub_checksum) + # raw bytes format - BIDENT2 + raw + return "0x0100" + bascii_to_str(hexlify(raw).upper()) + + def _calc_checksum(self, secret): + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + salt = self.salt + return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt) + + @classmethod + def verify(cls, secret, hash): + # NOTE: we only compare against the upper-case hash + # XXX: add 'full' just to verify both checksums? + uh.validate_secret(secret) + self = cls.from_string(hash) + chk = self.checksum + if chk is None: + raise uh.exc.MissingDigestError(cls) + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + result = _raw_mssql(secret.upper(), self.salt) + return consteq(result, chk[20:]) + +#============================================================================= +# handler +#============================================================================= +class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): + """This class implements the password hash used by MS-SQL 2005, and follows the :ref:`password-hash-api`. + + It supports a fixed-length salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + + :type salt: bytes + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 4 bytes in length. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + """ + #=================================================================== + # algorithm information + #=================================================================== + name = "mssql2005" + setting_kwds = ("salt",) + + checksum_size = 20 + min_salt_size = max_salt_size = 4 + _stub_checksum = b("\x00") * 20 + + #=================================================================== + # formatting + #=================================================================== + + # 0x0100 - 2 byte identifier + # 4 byte salt + # 20 byte checksum + # = 26 bytes + # encoded '0x' + 52 chars = 54 + + @classmethod + def identify(cls, hash): + return _ident_mssql(hash, 54, 26) + + @classmethod + def from_string(cls, hash): + data = _parse_mssql(hash, 54, 26, cls) + return cls(salt=data[:4], checksum=data[4:]) + + def to_string(self): + raw = self.salt + (self.checksum or self._stub_checksum) + # raw bytes format - BIDENT2 + raw + return "0x0100" + bascii_to_str(hexlify(raw)).upper() + + def _calc_checksum(self, secret): + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + return _raw_mssql(secret, self.salt) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/mysql.py passlib-1.6.1/passlib/handlers/mysql.py --- passlib-1.5.3/passlib/handlers/mysql.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/mysql.py 2012-08-01 17:10:03.000000000 +0000 @@ -19,128 +19,110 @@ Description taken from http://dev.mysql.com/doc/refman/6.0/en/password-hashing.html """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from hashlib import sha1 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -#pkg -from passlib.utils import handlers as uh, to_hash_str, b, bord, bytes -#local +# site +# pkg +from passlib.utils import to_native_str +from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, \ + byte_elem_value, str_to_uascii +import passlib.utils.handlers as uh +# local __all__ = [ 'mysql323', 'mysq41', ] -#========================================================= -#backend -#========================================================= +#============================================================================= +# backend +#============================================================================= class mysql323(uh.StaticHandler): """This class implements the MySQL 3.2.3 password hash, and follows the :ref:`password-hash-api`. It has no salt and a single fixed round. - The :meth:`encrypt()` and :meth:`genconfig` methods accept no optional keywords. + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== name = "mysql323" + checksum_size = 16 checksum_chars = uh.HEX_CHARS - _pat = re.compile(ur"^[0-9a-f]{16}$", re.I) - - #========================================================= - #methods - #========================================================= - + #=================================================================== + # methods + #=================================================================== @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) - - @classmethod - def genhash(cls, secret, config): - if config is not None and not cls.identify(config): - raise ValueError("not a mysql-3.2.3 hash") + def _norm_hash(cls, hash): + return hash.lower() - #FIXME: no idea if mysql has a policy about handling unicode passwords + def _calc_checksum(self, secret): + # FIXME: no idea if mysql has a policy about handling unicode passwords if isinstance(secret, unicode): secret = secret.encode("utf-8") MASK_32 = 0xffffffff MASK_31 = 0x7fffffff + WHITE = b(' \t') nr1 = 0x50305735 nr2 = 0x12345671 add = 7 for c in secret: - if c in b(' \t'): + if c in WHITE: continue - tmp = bord(c) + tmp = byte_elem_value(c) nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32 nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32 add = (add+tmp) & MASK_32 - hash = u"%08x%08x" % (nr1 & MASK_31, nr2 & MASK_31) - return to_hash_str(hash) - - @classmethod - def _norm_hash(cls, hash): - if isinstance(hash, bytes): - hash = hash.decode("ascii") - return hash.lower() + return u("%08x%08x") % (nr1 & MASK_31, nr2 & MASK_31) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#handler -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# handler +#============================================================================= class mysql41(uh.StaticHandler): """This class implements the MySQL 4.1 password hash, and follows the :ref:`password-hash-api`. It has no salt and a single fixed round. - The :meth:`encrypt()` and :meth:`genconfig` methods accept no optional keywords. + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== name = "mysql41" - _pat = re.compile(r"^\*[0-9A-F]{40}$", re.I) - - #========================================================= - #methods - #========================================================= + _hash_prefix = u("*") + checksum_chars = uh.HEX_CHARS + checksum_size = 40 + #=================================================================== + # methods + #=================================================================== @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + def _norm_hash(cls, hash): + return hash.upper() - @classmethod - def genhash(cls, secret, config): - if config is not None and not cls.identify(config): - raise ValueError("not a mysql-4.1 hash") - #FIXME: no idea if mysql has a policy about handling unicode passwords + def _calc_checksum(self, secret): + # FIXME: no idea if mysql has a policy about handling unicode passwords if isinstance(secret, unicode): secret = secret.encode("utf-8") - return '*' + sha1(sha1(secret).digest()).hexdigest().upper() - - @classmethod - def _norm_hash(cls, hash): - if isinstance(hash, bytes): - hash = hash.decode("ascii") - return hash.upper() + return str_to_uascii(sha1(sha1(secret).digest()).hexdigest()).upper() - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/nthash.py passlib-1.6.1/passlib/handlers/nthash.py --- passlib-1.5.3/passlib/handlers/nthash.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/nthash.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,101 +0,0 @@ -"""passlib.handlers.nthash - unix-crypt compatible nthash passwords""" -#========================================================= -#imports -#========================================================= -#core -import re -import logging; log = logging.getLogger(__name__) -from warnings import warn -#site -#libs -from passlib.utils import handlers as uh, to_unicode, to_hash_str, to_bytes, bytes -from passlib.utils.md4 import md4 -#pkg -#local -__all__ = [ - "NTHash", -] - -#========================================================= -#handler -#========================================================= -class nthash(uh.HasManyIdents, uh.GenericHandler): - """This class implements the NT Password hash in a manner compatible with the :ref:`modular-crypt-format`, and follows the :ref:`password-hash-api`. - - It has no salt and a single fixed round. - - The :meth:`encrypt()` and :meth:`genconfig` methods accept no optional keywords. - """ - - #TODO: verify where $NT$ is being used. - ##:param ident: - ##This handler supports two different :ref:`modular-crypt-format` identifiers. - ##It defaults to ``3``, but users may specify the alternate ``NT`` identifier - ##which is used in some contexts. - - #========================================================= - #class attrs - #========================================================= - #--GenericHandler-- - name = "nthash" - setting_kwds = ("ident",) - checksum_chars = uh.LC_HEX_CHARS - - _stub_checksum = u"0" * 32 - - #--HasManyIdents-- - default_ident = u"$3$$" - ident_values = (u"$3$$", u"$NT$") - ident_aliases = {u"3": u"$3$$", u"NT": u"$NT$"} - - #========================================================= - #formatting - #========================================================= - - @classmethod - def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - for ident in cls.ident_values: - if hash.startswith(ident): - break - else: - raise ValueError("invalid nthash") - chk = hash[len(ident):] - return cls(ident=ident, checksum=chk, strict=True) - - def to_string(self): - hash = self.ident + (self.checksum or self._stub_checksum) - return to_hash_str(hash) - - #========================================================= - #primary interface - #========================================================= - - def calc_checksum(self, secret): - return self.raw_nthash(secret, hex=True) - - @staticmethod - def raw_nthash(secret, hex=False): - """encode password using md4-based NTHASH algorithm - - :returns: - returns string of raw bytes if ``hex=False``, - returns digest as hexidecimal unicode if ``hex=True``. - """ - secret = to_unicode(secret, "utf-8") - hash = md4(secret.encode("utf-16le")) - if hex: - return to_unicode(hash.hexdigest(), 'ascii') - else: - return hash.digest() - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= diff -Nru passlib-1.5.3/passlib/handlers/oracle.py passlib-1.6.1/passlib/handlers/oracle.py --- passlib-1.5.3/passlib/handlers/oracle.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/oracle.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,28 +1,29 @@ """passlib.handlers.oracle - Oracle DB Password Hashes""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from binascii import hexlify, unhexlify from hashlib import sha1 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -#pkg -from passlib.utils import xor_bytes, handlers as uh, bytes, to_unicode, \ - to_hash_str, b +# site +# pkg +from passlib.utils import to_unicode, to_native_str, xor_bytes +from passlib.utils.compat import b, bytes, bascii_to_str, irange, u, \ + uascii_to_str, unicode, str_to_uascii from passlib.utils.des import des_encrypt_block -#local +import passlib.utils.handlers as uh +# local __all__ = [ "oracle10g", "oracle11g" ] -#========================================================= -#oracle10 -#========================================================= +#============================================================================= +# oracle10 +#============================================================================= def des_cbc_encrypt(key, value, iv=b('\x00') * 8, pad=b('\x00')): """performs des-cbc encryption, returns only last block. @@ -39,159 +40,136 @@ :returns: last block of DES-CBC encryption of all ``value``'s byte blocks. """ - value += pad * (-len(value) % 8) #null pad to multiple of 8 - hash = iv #start things off - for offset in xrange(0,len(value),8): + value += pad * (-len(value) % 8) # null pad to multiple of 8 + hash = iv # start things off + for offset in irange(0,len(value),8): chunk = xor_bytes(hash, value[offset:offset+8]) hash = des_encrypt_block(key, chunk) return hash -#: magic string used as initial des key by oracle10 +# magic string used as initial des key by oracle10 ORACLE10_MAGIC = b("\x01\x23\x45\x67\x89\xAB\xCD\xEF") -class oracle10(uh.StaticHandler): +class oracle10(uh.HasUserContext, uh.StaticHandler): """This class implements the password hash used by Oracle up to version 10g, and follows the :ref:`password-hash-api`. - It has no salt and a single fixed round. + It does a single round of hashing, and relies on the username as the salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept no optional keywords. - - The :meth:`encrypt()`, :meth:`genhash()`, and :meth:`verify()` methods all require the + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the following additional contextual keywords: - :param user: string containing name of oracle user account this password is associated with. + :type user: str + :param user: name of oracle user account this password is associated with. """ - #========================================================= - #algorithm information - #========================================================= + #=================================================================== + # algorithm information + #=================================================================== name = "oracle10" - setting_kwds = () - context_kwds = ("user",) - - #========================================================= - #formatting - #========================================================= - _pat = re.compile(ur"^[0-9a-fA-F]{16}$") + checksum_chars = uh.HEX_CHARS + checksum_size = 16 + #=================================================================== + # methods + #=================================================================== @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + def _norm_hash(cls, hash): + return hash.upper() - #========================================================= - #primary interface - #========================================================= - @classmethod - def genhash(cls, secret, config, user): - if config is not None and not cls.identify(config): - raise ValueError("not an oracle-10g hash") - if secret is None: - raise TypeError("secret must be specified") - if not user: - raise ValueError("user keyword must be specified for this algorithm") - - #FIXME: not sure how oracle handles unicode. - # online docs about 10g hash indicate it puts ascii chars - # in a 2-byte encoding w/ the high bytenull. - # they don't say how it handles other chars, - # or what encoding. + def _calc_checksum(self, secret): + # FIXME: not sure how oracle handles unicode. + # online docs about 10g hash indicate it puts ascii chars + # in a 2-byte encoding w/ the high byte set to null. + # they don't say how it handles other chars, or what encoding. # - # so for now, encoding secret & user to utf-16-be, - # since that fits, - # and if secret/user is bytes, we assume utf-8, and decode first. + # so for now, encoding secret & user to utf-16-be, + # since that fits, and if secret/user is bytes, + # we assume utf-8, and decode first. # - # this whole mess really needs someone w/ an oracle system, - # and some answers :) - - def encode(value): - "encode according to guess at how oracle encodes strings (see note above)" - #we can't trust what original encoding was. - #user should have passed us unicode in the first place. - #but try decoding as utf-8 just to work for most common case. - value = to_unicode(value, "utf-8") - return value.upper().encode("utf-16-be") - - input = encode(user) + encode(secret) + # this whole mess really needs someone w/ an oracle system, + # and some answers :) + if isinstance(secret, bytes): + secret = secret.decode("utf-8") + user = to_unicode(self.user, "utf-8", param="user") + input = (user+secret).upper().encode("utf-16-be") hash = des_cbc_encrypt(ORACLE10_MAGIC, input) hash = des_cbc_encrypt(hash, input) - return to_hash_str(hexlify(hash)).upper() + return hexlify(hash).decode("ascii").upper() - @classmethod - def _norm_hash(cls, hash): - if isinstance(hash, bytes): - hash = hash.decode("ascii") - return hash.upper() - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#oracle11 -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# oracle11 +#============================================================================= class oracle11(uh.HasSalt, uh.GenericHandler): """This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 20 hexidecimal characters. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "oracle11" setting_kwds = ("salt",) checksum_size = 40 - checksum_chars = uh.UC_HEX_CHARS + checksum_chars = uh.UPPER_HEX_CHARS - _stub_checksum = u'0' * 40 + _stub_checksum = u('0') * 40 #--HasSalt-- min_salt_size = max_salt_size = 20 - salt_chars = uh.UC_HEX_CHARS - + salt_chars = uh.UPPER_HEX_CHARS - #========================================================= - #methods - #========================================================= - _pat = re.compile(u"^S:(?P[0-9a-f]{40})(?P[0-9a-f]{20})$", re.I) - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, cls._pat) + #=================================================================== + # methods + #=================================================================== + _hash_regex = re.compile(u("^S:(?P[0-9a-f]{40})(?P[0-9a-f]{20})$"), re.I) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash provided") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._pat.match(hash) + hash = to_unicode(hash, "ascii", "hash") + m = cls._hash_regex.match(hash) if not m: - raise ValueError("invalid oracle-11g hash") + raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") - return cls(salt=salt, checksum=chk.upper(), strict=True) + return cls(salt=salt, checksum=chk.upper()) def to_string(self): chk = (self.checksum or self._stub_checksum) - hash = u"S:%s%s" % (chk.upper(), self.salt.upper()) - return to_hash_str(hash) + hash = u("S:%s%s") % (chk.upper(), self.salt.upper()) + return uascii_to_str(hash) - def calc_checksum(self, secret): + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest() - return to_unicode(chk, 'ascii').upper() + return str_to_uascii(chk).upper() - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/pbkdf2.py passlib-1.6.1/passlib/handlers/pbkdf2.py --- passlib-1.5.3/passlib/handlers/pbkdf2.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/pbkdf2.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,20 +1,20 @@ """passlib.handlers.pbkdf - PBKDF2 based hashes""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from binascii import hexlify, unhexlify from base64 import b64encode, b64decode import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import adapted_b64_encode, adapted_b64_decode, \ - handlers as uh, to_hash_str, to_unicode, bytes, b +# site +# pkg +from passlib.utils import ab64_decode, ab64_encode, to_unicode +from passlib.utils.compat import b, bytes, str_to_bascii, u, uascii_to_str, unicode from passlib.utils.pbkdf2 import pbkdf2 -#pkg -#local +import passlib.utils.handlers as uh +# local __all__ = [ "pbkdf2_sha1", "pbkdf2_sha256", @@ -24,18 +24,18 @@ "grub_pbkdf2_sha512", ] -#========================================================= +#============================================================================= # -#========================================================= +#============================================================================= class Pbkdf2DigestHandler(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): "base class for various pbkdf2_{digest} algorithms" - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- setting_kwds = ("salt", "salt_size", "rounds") - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS #--HasSalt-- default_salt_size = 16 @@ -43,106 +43,111 @@ max_salt_size = 1024 #--HasRounds-- - default_rounds = 6400 + default_rounds = None # set by subclass min_rounds = 1 - max_rounds = 2**32-1 + max_rounds = 0xffffffff # setting at 32-bit limit for now rounds_cost = "linear" #--this class-- - _prf = None #subclass specified prf identifier + _prf = None # subclass specified prf identifier - #NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. - # the underlying pbkdf2 specifies no bounds for either. + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. + # the underlying pbkdf2 specifies no bounds for either. - #NOTE: defaults chosen to be at least as large as pbkdf2 rfc recommends... - # >8 bytes of entropy in salt, >1000 rounds - # increased due to time since rfc established - - #========================================================= - #methods - #========================================================= + # NOTE: defaults chosen to be at least as large as pbkdf2 rfc recommends... + # >8 bytes of entropy in salt, >1000 rounds + # increased due to time since rfc established + + #=================================================================== + # methods + #=================================================================== @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - rounds, salt, chk = uh.parse_mc3(hash, cls.ident, cls.name) - int_rounds = int(rounds) - if rounds != unicode(int_rounds): #forbid zero padding, etc. - raise ValueError("invalid %s hash" % (cls.name,)) - raw_salt = adapted_b64_decode(salt.encode("ascii")) - raw_chk = adapted_b64_decode(chk.encode("ascii")) if chk else None - return cls( - rounds=int_rounds, - salt=raw_salt, - checksum=raw_chk, - strict=bool(raw_chk), - ) + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + salt = ab64_decode(salt.encode("ascii")) + if chk: + chk = ab64_decode(chk.encode("ascii")) + return cls(rounds=rounds, salt=salt, checksum=chk) def to_string(self, withchk=True): - salt = adapted_b64_encode(self.salt).decode("ascii") + salt = ab64_encode(self.salt).decode("ascii") if withchk and self.checksum: - chk = adapted_b64_encode(self.checksum).decode("ascii") - hash = u'%s%d$%s$%s' % (self.ident, self.rounds, salt, chk) + chk = ab64_encode(self.checksum).decode("ascii") else: - hash = u'%s%d$%s' % (self.ident, self.rounds, salt) - return to_hash_str(hash) + chk = None + return uh.render_mc3(self.ident, self.rounds, salt, chk) - def calc_checksum(self, secret): + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") return pbkdf2(secret, self.salt, self.rounds, self.checksum_size, self._prf) -def create_pbkdf2_hash(hash_name, digest_size, ident=None): +def create_pbkdf2_hash(hash_name, digest_size, rounds=12000, ident=None, module=__name__): "create new Pbkdf2DigestHandler subclass for a specific hash" name = 'pbkdf2_' + hash_name if ident is None: - ident = u"$pbkdf2-%s$" % (hash_name,) + ident = u("$pbkdf2-%s$") % (hash_name,) prf = "hmac-%s" % (hash_name,) base = Pbkdf2DigestHandler return type(name, (base,), dict( + __module__=module, # so ABCMeta won't clobber it. name=name, ident=ident, _prf = prf, + default_rounds=rounds, checksum_size=digest_size, encoded_checksum_size=(digest_size*4+2)//3, __doc__="""This class implements a generic ``PBKDF2-%(prf)s``-based password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: bytes :param salt: Optional salt bytes. If specified, the length must be between 0-1024 bytes. If not specified, a %(dsc)d byte salt will be autogenerated (this is recommended). + :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 16 bytes, but can be any value between 0 and 1024. + :type rounds: int :param rounds: Optional number of rounds to use. Defaults to %(dr)d, but must be within ``range(1,1<<32)``. - """ % dict(prf=prf.upper(), dsc=base.default_salt_size, dr=base.default_rounds) + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + """ % dict(prf=prf.upper(), dsc=base.default_salt_size, dr=rounds) )) -#--------------------------------------------------------- -#derived handlers -#--------------------------------------------------------- -pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20, ident=u"$pbkdf2$") +#------------------------------------------------------------------------ +# derived handlers +#------------------------------------------------------------------------ +pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20, 60000, ident=u("$pbkdf2$")) pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32) pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64) -ldap_pbkdf2_sha1 = uh.PrefixWrapper("ldap_pbkdf2_sha1", pbkdf2_sha1, "{PBKDF2}", "$pbkdf2$") -ldap_pbkdf2_sha256 = uh.PrefixWrapper("ldap_pbkdf2_sha256", pbkdf2_sha256, "{PBKDF2-SHA256}", "$pbkdf2-sha256$") -ldap_pbkdf2_sha512 = uh.PrefixWrapper("ldap_pbkdf2_sha512", pbkdf2_sha512, "{PBKDF2-SHA512}", "$pbkdf2-sha512$") - -#========================================================= -#cryptacular's pbkdf2 hash -#========================================================= +ldap_pbkdf2_sha1 = uh.PrefixWrapper("ldap_pbkdf2_sha1", pbkdf2_sha1, "{PBKDF2}", "$pbkdf2$", ident=True) +ldap_pbkdf2_sha256 = uh.PrefixWrapper("ldap_pbkdf2_sha256", pbkdf2_sha256, "{PBKDF2-SHA256}", "$pbkdf2-sha256$", ident=True) +ldap_pbkdf2_sha512 = uh.PrefixWrapper("ldap_pbkdf2_sha512", pbkdf2_sha512, "{PBKDF2-SHA512}", "$pbkdf2-sha512$", ident=True) + +#============================================================================= +# cryptacular's pbkdf2 hash +#============================================================================= -#: bytes used by cta hash for base64 values 63 & 64 +# bytes used by cta hash for base64 values 63 & 64 CTA_ALTCHARS = b("-_") class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): @@ -150,210 +155,228 @@ It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: bytes :param salt: Optional salt bytes. If specified, it may be any length. If not specified, a one will be autogenerated (this is recommended). + :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 16 bytes, but can be any value between 0 and 1024. + :type rounds: int :param rounds: Optional number of rounds to use. - Defaults to 10000, must be within ``range(1,1<<32)``. + Defaults to 60000, must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "cta_pbkdf2_sha1" setting_kwds = ("salt", "salt_size", "rounds") - ident = u"$p5k2$" + ident = u("$p5k2$") - #NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. - # underlying algorithm (and reference implementation) allow effectively unbounded values for both of these. + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. underlying algorithm (and reference implementation) + # allows effectively unbounded values for both of these parameters. #--HasSalt-- default_salt_size = 16 min_salt_size = 0 max_salt_size = 1024 - #--HasROunds-- - default_rounds = 10000 - min_rounds = 0 - max_rounds = 2**32-1 + #--HasRounds-- + default_rounds = 60000 + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now rounds_cost = "linear" - #========================================================= - #formatting - #========================================================= - - #hash $p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0= - #ident $p5k2$ - #rounds 1000 - #salt ZxK4ZBJCfQg= - #chk jJZVscWtO--p1-xIZl6jhO2LKR0= - #NOTE: rounds in hex + #=================================================================== + # formatting + #=================================================================== + + # hash $p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0= + # ident $p5k2$ + # rounds 1000 + # salt ZxK4ZBJCfQg= + # chk jJZVscWtO--p1-xIZl6jhO2LKR0= + # NOTE: rounds in hex @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - rounds, salt, chk = uh.parse_mc3(hash, cls.ident, cls.name) - if rounds.startswith("0"): - #passlib deviation: forbidding - #left-padded with zeroes - raise ValueError("invalid cta_pbkdf2_sha1 hash") - rounds = int(rounds, 16) + # NOTE: passlib deviation - forbidding zero-padded rounds + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, handler=cls) salt = b64decode(salt.encode("ascii"), CTA_ALTCHARS) if chk: chk = b64decode(chk.encode("ascii"), CTA_ALTCHARS) - return cls( - rounds=rounds, - salt=salt, - checksum=chk, - strict=bool(chk), - ) + return cls(rounds=rounds, salt=salt, checksum=chk) def to_string(self, withchk=True): - out = u'$p5k2$%x$%s' % (self.rounds, - b64encode(self.salt, CTA_ALTCHARS).decode("ascii")) + salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii") if withchk and self.checksum: - out = u"%s$%s" % (out, - b64encode(self.checksum, CTA_ALTCHARS).decode("ascii")) - return to_hash_str(out) - - #========================================================= - #backend - #========================================================= - def calc_checksum(self, secret): + chk = b64encode(self.checksum, CTA_ALTCHARS).decode("ascii") + else: + chk = None + return uh.render_mc3(self.ident, self.rounds, salt, chk, rounds_base=16) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") return pbkdf2(secret, self.salt, self.rounds, 20, "hmac-sha1") - #========================================================= - #eoc - #========================================================= - -#========================================================= -#dlitz's pbkdf2 hash -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# dlitz's pbkdf2 hash +#============================================================================= class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements Dwayne Litzenberger's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If specified, it may be any length, but must use the characters in the regexp range ``[./0-9A-Za-z]``. If not specified, a 16 character salt will be autogenerated (this is recommended). + :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 16 bytes, but can be any value between 0 and 1024. + :type rounds: int :param rounds: Optional number of rounds to use. - Defaults to 10000, must be within ``range(1,1<<32)``. + Defaults to 60000, must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "dlitz_pbkdf2_sha1" setting_kwds = ("salt", "salt_size", "rounds") - ident = u"$p5k2$" + ident = u("$p5k2$") - #NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. - # underlying algorithm (and reference implementation) allow effectively unbounded values for both of these. + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. underlying algorithm (and reference implementation) + # allows effectively unbounded values for both of these parameters. #--HasSalt-- default_salt_size = 16 min_salt_size = 0 max_salt_size = 1024 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - #--HasROunds-- - default_rounds = 10000 - min_rounds = 0 - max_rounds = 2**32-1 + #--HasRounds-- + default_rounds = 60000 + min_rounds = 1 + max_rounds = 0xffffffff # setting at 32-bit limit for now rounds_cost = "linear" - #========================================================= - #formatting - #========================================================= - - #hash $p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g - #ident $p5k2$ - #rounds c - #salt u9HvcT4d - #chk Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g - #rounds in lowercase hex, no zero padding + #=================================================================== + # formatting + #=================================================================== + + # hash $p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + # ident $p5k2$ + # rounds c + # salt u9HvcT4d + # chk Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + # rounds in lowercase hex, no zero padding @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - rounds, salt, chk = uh.parse_mc3(hash, cls.ident, cls.name) - if rounds.startswith("0"): #zero not allowed, nor left-padded with zeroes - raise ValueError("invalid dlitz_pbkdf2_sha1 hash") - rounds = int(rounds, 16) if rounds else 400 - return cls( - rounds=rounds, - salt=salt, - checksum=chk, - strict=bool(chk), - ) - - def to_string(self, withchk=True, native=True): - if self.rounds == 400: - out = u'$p5k2$$%s' % (self.salt,) - else: - out = u'$p5k2$%x$%s' % (self.rounds, self.salt) - if withchk and self.checksum: - out = u"%s$%s" % (out,self.checksum) - return to_hash_str(out) if native else out + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16, + default_rounds=400, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) - #========================================================= - #backend - #========================================================= - def calc_checksum(self, secret): + def to_string(self, withchk=True): + rounds = self.rounds + if rounds == 400: + rounds = None # omit rounds measurement if == 400 + return uh.render_mc3(self.ident, rounds, self.salt, + checksum=self.checksum if withchk else None, + rounds_base=16) + + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - salt = self.to_string(withchk=False, native=False).encode("ascii") + salt = str_to_bascii(self.to_string(withchk=False)) result = pbkdf2(secret, salt, self.rounds, 24, "hmac-sha1") - return adapted_b64_encode(result).decode("ascii") + return ab64_encode(result).decode("ascii") - #========================================================= - #eoc - #========================================================= - -#========================================================= -#crowd -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# crowd +#============================================================================= class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class implements the PBKDF2 hash used by Atlassian. It supports a fixed-length salt, and a fixed number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keyword: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword: + :type salt: bytes :param salt: Optional salt bytes. If specified, the length must be exactly 16 bytes. If not specified, a salt will be autogenerated (this is recommended). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include + ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ #--GenericHandler-- name = "atlassian_pbkdf2_sha1" setting_kwds =("salt",) - ident = u"{PKCS5S2}" + ident = u("{PKCS5S2}") checksum_size = 32 _stub_checksum = b("\x00") * 32 @@ -363,102 +386,103 @@ @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") + hash = to_unicode(hash, "ascii", "hash") ident = cls.ident if not hash.startswith(ident): - raise ValueError("invalid %s hash" % (cls.name,)) + raise uh.exc.InvalidHashError(cls) data = b64decode(hash[len(ident):].encode("ascii")) salt, chk = data[:16], data[16:] - return cls(salt=salt, checksum=chk, strict=True) + return cls(salt=salt, checksum=chk) def to_string(self): data = self.salt + (self.checksum or self._stub_checksum) hash = self.ident + b64encode(data).decode("ascii") - return to_hash_str(hash) + return uascii_to_str(hash) - def calc_checksum(self, secret): - #TODO: find out what crowd's policy is re: unicode + def _calc_checksum(self, secret): + # TODO: find out what crowd's policy is re: unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") - #crowd seems to use a fixed number of rounds. + # crowd seems to use a fixed number of rounds. return pbkdf2(secret, self.salt, 10000, 32, "hmac-sha1") -#========================================================= -#grub -#========================================================= +#============================================================================= +# grub +#============================================================================= class grub_pbkdf2_sha512(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class implements Grub's pbkdf2-hmac-sha512 hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: bytes :param salt: Optional salt bytes. If specified, the length must be between 0-1024 bytes. If not specified, a 64 byte salt will be autogenerated (this is recommended). + :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 64 bytes, but can be any value between 0 and 1024. + :type rounds: int :param rounds: Optional number of rounds to use. - Defaults to 10000, but must be within ``range(1,1<<32)``. + Defaults to 12000, but must be within ``range(1,1<<32)``. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ name = "grub_pbkdf2_sha512" setting_kwds = ("salt", "salt_size", "rounds") - ident = u"grub.pbkdf2.sha512." + ident = u("grub.pbkdf2.sha512.") - #NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide sanity check. - # the underlying pbkdf2 specifies no bounds for either, - # and it's not clear what grub specifies. + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide a + # sanity check. the underlying pbkdf2 specifies no bounds for either, + # and it's not clear what grub specifies. default_salt_size = 64 min_salt_size = 0 max_salt_size = 1024 - default_rounds = 10000 + default_rounds = 12000 min_rounds = 1 - max_rounds = 2**32-1 + max_rounds = 0xffffffff # setting at 32-bit limit for now rounds_cost = "linear" @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - rounds, salt, chk = uh.parse_mc3(hash, cls.ident, cls.name, sep=u".") - int_rounds = int(rounds) - if rounds != str(int_rounds): #forbid zero padding, etc. - raise ValueError("invalid %s hash" % (cls.name,)) - raw_salt = unhexlify(salt.encode("ascii")) - raw_chk = unhexlify(chk.encode("ascii")) if chk else None - return cls( - rounds=int_rounds, - salt=raw_salt, - checksum=raw_chk, - strict=bool(raw_chk), - ) + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, sep=u("."), + handler=cls) + salt = unhexlify(salt.encode("ascii")) + if chk: + chk = unhexlify(chk.encode("ascii")) + return cls(rounds=rounds, salt=salt, checksum=chk) def to_string(self, withchk=True): salt = hexlify(self.salt).decode("ascii").upper() if withchk and self.checksum: chk = hexlify(self.checksum).decode("ascii").upper() - hash = u'%s%d.%s.%s' % (self.ident, self.rounds, salt, chk) else: - hash = u'%s%d.%s' % (self.ident, self.rounds, salt) - return to_hash_str(hash) + chk = None + return uh.render_mc3(self.ident, self.rounds, salt, chk, sep=u(".")) - def calc_checksum(self, secret): - #TODO: find out what grub's policy is re: unicode + def _calc_checksum(self, secret): + # TODO: find out what grub's policy is re: unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") return pbkdf2(secret, self.salt, self.rounds, 64, "hmac-sha512") -#========================================================= -#eof -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/phpass.py passlib-1.6.1/passlib/handlers/phpass.py --- passlib-1.5.3/passlib/handlers/phpass.py 2011-10-08 04:51:13.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/phpass.py 2012-08-01 17:10:03.000000000 +0000 @@ -5,77 +5,89 @@ phpass context - blowfish, bsdi_crypt, phpass """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import h64, handlers as uh, bytes, b, to_unicode, to_hash_str -#pkg -#local +# site +# pkg +from passlib.utils import h64 +from passlib.utils.compat import b, bytes, u, uascii_to_str, unicode +import passlib.utils.handlers as uh +# local __all__ = [ "phpass", ] -#========================================================= -#phpass -#========================================================= +#============================================================================= +# phpass +#============================================================================= class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the PHPass Portable Hash, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 8 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + :type rounds: int :param rounds: Optional number of rounds to use. - Defaults to 9, must be between 7 and 30, inclusive. + Defaults to 16, must be between 7 and 30, inclusive. This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`. + :type ident: str :param ident: phpBB3 uses ``H`` instead of ``P`` for it's identifier, this may be set to ``H`` in order to generate phpBB3 compatible hashes. it defaults to ``P``. + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "phpass" setting_kwds = ("salt", "rounds", "ident") - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 8 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS #--HasRounds-- - default_rounds = 9 + default_rounds = 16 min_rounds = 7 max_rounds = 30 rounds_cost = "log2" - _strict_rounds_bounds = True #--HasManyIdents-- - default_ident = u"$P$" - ident_values = [u"$P$", u"$H$"] - ident_aliases = {u"P":u"$P$", u"H":u"$H$"} - - #========================================================= - #formatting - #========================================================= + default_ident = u("$P$") + ident_values = [u("$P$"), u("$H$")] + ident_aliases = {u("P"):u("$P$"), u("H"):u("$H$")} + + #=================================================================== + # formatting + #=================================================================== #$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0 # $P$ @@ -85,37 +97,27 @@ @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode('ascii') - for ident in cls.ident_values: - if hash.startswith(ident): - break - else: - raise ValueError("invalid phpass portable hash") - data = hash[len(ident):] + ident, data = cls._parse_ident(hash) rounds, salt, chk = data[0], data[1:9], data[9:] return cls( ident=ident, rounds=h64.decode_int6(rounds.encode("ascii")), salt=salt, - checksum=chk, - strict=bool(chk), + checksum=chk or None, ) def to_string(self): - hash = u"%s%s%s%s" % (self.ident, + hash = u("%s%s%s%s") % (self.ident, h64.encode_int6(self.rounds).decode("ascii"), self.salt, - self.checksum or u'') - return to_hash_str(hash) + self.checksum or u('')) + return uascii_to_str(hash) - #========================================================= - #backend - #========================================================= - def calc_checksum(self, secret): - #FIXME: can't find definitive policy on how phpass handles non-ascii. + #=================================================================== + # backend + #=================================================================== + def _calc_checksum(self, secret): + # FIXME: can't find definitive policy on how phpass handles non-ascii. if isinstance(secret, unicode): secret = secret.encode("utf-8") real_rounds = 1<`_ + hash names. + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 + + In addition to the standard :ref:`password-hash-api` methods, + this class also provides the following methods for manipulating Passlib + scram hashes in ways useful for pluging into a SCRAM protocol stack: + + .. automethod:: extract_digest_info + .. automethod:: extract_digest_algs + .. automethod:: derive_digest + """ + #=================================================================== + # class attrs + #=================================================================== + + # NOTE: unlike most GenericHandler classes, the 'checksum' attr of + # ScramHandler is actually a map from digest_name -> digest, so + # many of the standard methods have been overridden. + + # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide + # a sanity check; the underlying pbkdf2 specifies no bounds for either. + + #--GenericHandler-- + name = "scram" + setting_kwds = ("salt", "salt_size", "rounds", "algs") + ident = u("$scram$") + + #--HasSalt-- + default_salt_size = 12 + min_salt_size = 0 + max_salt_size = 1024 + + #--HasRounds-- + default_rounds = 6400 + min_rounds = 1 + max_rounds = 2**32-1 + rounds_cost = "linear" + + #--custom-- + + # default algorithms when creating new hashes. + default_algs = ["sha-1", "sha-256", "sha-512"] + + # list of algs verify prefers to use, in order. + _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"] + + #=================================================================== + # instance attrs + #=================================================================== + + # 'checksum' is different from most GenericHandler subclasses, + # in that it contains a dict mapping from alg -> digest, + # or None if no checksum present. + + # list of algorithms to create/compare digests for. + algs = None + + #=================================================================== + # scram frontend helpers + #=================================================================== + @classmethod + def extract_digest_info(cls, hash, alg): + """return (salt, rounds, digest) for specific hash algorithm. + + :type hash: str + :arg hash: + :class:`!scram` hash stored for desired user + + :type alg: str + :arg alg: + Name of digest algorithm (e.g. ``"sha-1"``) requested by client. + + This value is run through :func:`~passlib.utils.pbkdf2.norm_hash_name`, + so it is case-insensitive, and can be the raw SCRAM + mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name, + or the hashlib name. + + :raises KeyError: + If the hash does not contain an entry for the requested digest + algorithm. + + :returns: + A tuple containing ``(salt, rounds, digest)``, + where *digest* matches the raw bytes returned by + SCRAM's :func:`Hi` function for the stored password, + the provided *salt*, and the iteration count (*rounds*). + *salt* and *digest* are both raw (unencoded) bytes. + """ + # XXX: this could be sped up by writing custom parsing routine + # that just picks out relevant digest, and doesn't bother + # with full structure validation each time it's called. + alg = norm_hash_name(alg, 'iana') + self = cls.from_string(hash) + chkmap = self.checksum + if not chkmap: + raise ValueError("scram hash contains no digests") + return self.salt, self.rounds, chkmap[alg] + + @classmethod + def extract_digest_algs(cls, hash, format="iana"): + """Return names of all algorithms stored in a given hash. + + :type hash: str + :arg hash: + The :class:`!scram` hash to parse + + :type format: str + :param format: + This changes the naming convention used by the + returned algorithm names. By default the names + are IANA-compatible; see :func:`~passlib.utils.pbkdf2.norm_hash_name` + for possible values. + + :returns: + Returns a list of digest algorithms; e.g. ``["sha-1"]`` + """ + # XXX: this could be sped up by writing custom parsing routine + # that just picks out relevant names, and doesn't bother + # with full structure validation each time it's called. + algs = cls.from_string(hash).algs + if format == "iana": + return algs + else: + return [norm_hash_name(alg, format) for alg in algs] + + @classmethod + def derive_digest(cls, password, salt, rounds, alg): + """helper to create SaltedPassword digest for SCRAM. + + This performs the step in the SCRAM protocol described as:: + + SaltedPassword := Hi(Normalize(password), salt, i) + + :type password: unicode or utf-8 bytes + :arg password: password to run through digest + + :type salt: bytes + :arg salt: raw salt data + + :type rounds: int + :arg rounds: number of iterations. + + :type alg: str + :arg alg: name of digest to use (e.g. ``"sha-1"``). + + :returns: + raw bytes of ``SaltedPassword`` + """ + if isinstance(password, bytes): + password = password.decode("utf-8") + password = saslprep(password).encode("utf-8") + if not isinstance(salt, bytes): + raise TypeError("salt must be bytes") + if rounds < 1: + raise ValueError("rounds must be >= 1") + alg = norm_hash_name(alg, "hashlib") + return pbkdf2(password, salt, rounds, None, "hmac-" + alg) + + #=================================================================== + # serialization + #=================================================================== + + @classmethod + def from_string(cls, hash): + hash = to_native_str(hash, "ascii", "hash") + if not hash.startswith("$scram$"): + raise uh.exc.InvalidHashError(cls) + parts = hash[7:].split("$") + if len(parts) != 3: + raise uh.exc.MalformedHashError(cls) + rounds_str, salt_str, chk_str = parts + + # decode rounds + rounds = int(rounds_str) + if rounds_str != str(rounds): # forbid zero padding, etc. + raise uh.exc.MalformedHashError(cls) + + # decode salt + try: + salt = ab64_decode(salt_str.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + + # decode algs/digest list + if not chk_str: + # scram hashes MUST have something here. + raise uh.exc.MalformedHashError(cls) + elif "=" in chk_str: + # comma-separated list of 'alg=digest' pairs + algs = None + chkmap = {} + for pair in chk_str.split(","): + alg, digest = pair.split("=") + try: + chkmap[alg] = ab64_decode(digest.encode("ascii")) + except TypeError: + raise uh.exc.MalformedHashError(cls) + else: + # comma-separated list of alg names, no digests + algs = chk_str + chkmap = None + + # return new object + return cls( + rounds=rounds, + salt=salt, + checksum=chkmap, + algs=algs, + ) + + def to_string(self, withchk=True): + salt = bascii_to_str(ab64_encode(self.salt)) + chkmap = self.checksum + if withchk and chkmap: + chk_str = ",".join( + "%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg]))) + for alg in self.algs + ) + else: + chk_str = ",".join(self.algs) + return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str) + + #=================================================================== + # init + #=================================================================== + def __init__(self, algs=None, **kwds): + super(scram, self).__init__(**kwds) + self.algs = self._norm_algs(algs) + + def _norm_checksum(self, checksum): + if checksum is None: + return None + for alg, digest in iteritems(checksum): + if alg != norm_hash_name(alg, 'iana'): + raise ValueError("malformed algorithm name in scram hash: %r" % + (alg,)) + if len(alg) > 9: + raise ValueError("SCRAM limits algorithm names to " + "9 characters: %r" % (alg,)) + if not isinstance(digest, bytes): + raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests") + # TODO: verify digest size (if digest is known) + if 'sha-1' not in checksum: + # NOTE: required because of SCRAM spec. + raise ValueError("sha-1 must be in algorithm list of scram hash") + return checksum + + def _norm_algs(self, algs): + "normalize algs parameter" + # determine default algs value + if algs is None: + # derive algs list from checksum (if present). + chk = self.checksum + if chk is not None: + return sorted(chk) + elif self.use_defaults: + return list(self.default_algs) + else: + raise TypeError("no algs list specified") + elif self.checksum is not None: + raise RuntimeError("checksum & algs kwds are mutually exclusive") + + # parse args value + if isinstance(algs, str): + algs = splitcomma(algs) + algs = sorted(norm_hash_name(alg, 'iana') for alg in algs) + if any(len(alg)>9 for alg in algs): + raise ValueError("SCRAM limits alg names to max of 9 characters") + if 'sha-1' not in algs: + # NOTE: required because of SCRAM spec (rfc 5802) + raise ValueError("sha-1 must be in algorithm list of scram hash") + return algs + + #=================================================================== + # digest methods + #=================================================================== + + @classmethod + def _bind_needs_update(cls, **settings): + "generate a deprecation detector for CryptContext to use" + # generate deprecation hook which marks hashes as deprecated + # if they don't support a superset of current algs. + algs = frozenset(cls(use_defaults=True, **settings).algs) + def detector(hash, secret): + return not algs.issubset(cls.from_string(hash).algs) + return detector + + def _calc_checksum(self, secret, alg=None): + rounds = self.rounds + salt = self.salt + hash = self.derive_digest + if alg: + # if requested, generate digest for specific alg + return hash(secret, salt, rounds, alg) + else: + # by default, return dict containing digests for all algs + return dict( + (alg, hash(secret, salt, rounds, alg)) + for alg in self.algs + ) + + @classmethod + def verify(cls, secret, hash, full=False): + uh.validate_secret(secret) + self = cls.from_string(hash) + chkmap = self.checksum + if not chkmap: + raise ValueError("expected %s hash, got %s config string instead" % + (cls.name, cls.name)) + + # NOTE: to make the verify method efficient, we just calculate hash + # of shortest digest by default. apps can pass in "full=True" to + # check entire hash for consistency. + if full: + correct = failed = False + for alg, digest in iteritems(chkmap): + other = self._calc_checksum(secret, alg) + # NOTE: could do this length check in norm_algs(), + # but don't need to be that strict, and want to be able + # to parse hashes containing algs not supported by platform. + # it's fine if we fail here though. + if len(digest) != len(other): + raise ValueError("mis-sized %s digest in scram hash: %r != %r" + % (alg, len(digest), len(other))) + if consteq(other, digest): + correct = True + else: + failed = True + if correct and failed: + raise ValueError("scram hash verified inconsistently, " + "may be corrupted") + else: + return correct + else: + # XXX: should this just always use sha1 hash? would be faster. + # otherwise only verify against one hash, pick one w/ best security. + for alg in self._verify_algs: + if alg in chkmap: + other = self._calc_checksum(secret, alg) + return consteq(other, chkmap[alg]) + # there should always be sha-1 at the very least, + # or something went wrong inside _norm_algs() + raise AssertionError("sha-1 digest not found!") + + #=================================================================== + # + #=================================================================== + +#============================================================================= +# code used for testing scram against protocol examples during development. +#============================================================================= +##def _test_reference_scram(): +## "quick hack testing scram reference vectors" +## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801 +## from passlib.utils.compat import print_ +## +## engine = _scram_engine( +## alg="sha-1", +## salt='QSXCR+Q6sek8bf92'.decode("base64"), +## rounds=4096, +## password=u("pencil"), +## ) +## print_(engine.digest.encode("base64").rstrip()) +## +## msg = engine.format_auth_msg( +## username="user", +## client_nonce = "fyko+d2lbbFgONRv9qkxdawL", +## server_nonce = "3rfcNHYJY1ZVvWVs7j", +## header='c=biws', +## ) +## +## cp = engine.get_encoded_client_proof(msg) +## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp +## +## ss = engine.get_encoded_server_sig(msg) +## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss +## +##class _scram_engine(object): +## """helper class for verifying scram hash behavior +## against SCRAM protocol examples. not officially part of Passlib. +## +## takes in alg, salt, rounds, and a digest or password. +## +## can calculate the various keys & messages of the scram protocol. +## +## """ +## #========================================================= +## # init +## #========================================================= +## +## @classmethod +## def from_string(cls, hash, alg): +## "create record from scram hash, for given alg" +## return cls(alg, *scram.extract_digest_info(hash, alg)) +## +## def __init__(self, alg, salt, rounds, digest=None, password=None): +## self.alg = norm_hash_name(alg) +## self.salt = salt +## self.rounds = rounds +## self.password = password +## if password: +## data = scram.derive_digest(password, salt, rounds, alg) +## if digest and data != digest: +## raise ValueError("password doesn't match digest") +## else: +## digest = data +## elif not digest: +## raise TypeError("must provide password or digest") +## self.digest = digest +## +## #========================================================= +## # frontend methods +## #========================================================= +## def get_hash(self, data): +## "return hash of raw data" +## return hashlib.new(iana_to_hashlib(self.alg), data).digest() +## +## def get_client_proof(self, msg): +## "return client proof of specified auth msg text" +## return xor_bytes(self.client_key, self.get_client_sig(msg)) +## +## def get_encoded_client_proof(self, msg): +## return self.get_client_proof(msg).encode("base64").rstrip() +## +## def get_client_sig(self, msg): +## "return client signature of specified auth msg text" +## return self.get_hmac(self.stored_key, msg) +## +## def get_server_sig(self, msg): +## "return server signature of specified auth msg text" +## return self.get_hmac(self.server_key, msg) +## +## def get_encoded_server_sig(self, msg): +## return self.get_server_sig(msg).encode("base64").rstrip() +## +## def format_server_response(self, client_nonce, server_nonce): +## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format( +## client_nonce=client_nonce, +## server_nonce=server_nonce, +## rounds=self.rounds, +## salt=self.encoded_salt, +## ) +## +## def format_auth_msg(self, username, client_nonce, server_nonce, +## header='c=biws'): +## return ( +## 'n={username},r={client_nonce}' +## ',' +## 'r={client_nonce}{server_nonce},s={salt},i={rounds}' +## ',' +## '{header},r={client_nonce}{server_nonce}' +## ).format( +## username=username, +## client_nonce=client_nonce, +## server_nonce=server_nonce, +## salt=self.encoded_salt, +## rounds=self.rounds, +## header=header, +## ) +## +## #========================================================= +## # helpers to calculate & cache constant data +## #========================================================= +## def _calc_get_hmac(self): +## return get_prf("hmac-" + iana_to_hashlib(self.alg))[0] +## +## def _calc_client_key(self): +## return self.get_hmac(self.digest, b("Client Key")) +## +## def _calc_stored_key(self): +## return self.get_hash(self.client_key) +## +## def _calc_server_key(self): +## return self.get_hmac(self.digest, b("Server Key")) +## +## def _calc_encoded_salt(self): +## return self.salt.encode("base64").rstrip() +## +## #========================================================= +## # hacks for calculated attributes +## #========================================================= +## +## def __getattr__(self, attr): +## if not attr.startswith("_"): +## f = getattr(self, "_calc_" + attr, None) +## if f: +## value = f() +## setattr(self, attr, value) +## return value +## raise AttributeError("attribute not found") +## +## def __dir__(self): +## cdir = dir(self.__class__) +## attrs = set(cdir) +## attrs.update(self.__dict__) +## attrs.update(attr[6:] for attr in cdir +## if attr.startswith("_calc_")) +## return sorted(attrs) +## #========================================================= +## # eoc +## #========================================================= + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/sha1_crypt.py passlib-1.6.1/passlib/handlers/sha1_crypt.py --- passlib-1.5.3/passlib/handlers/sha1_crypt.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/sha1_crypt.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,122 +1,124 @@ """passlib.handlers.sha1_crypt """ -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= -#core +# core from hmac import new as hmac from hashlib import sha1 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import h64, handlers as uh, safe_os_crypt, classproperty, \ - to_hash_str, to_unicode, bytes, b -from passlib.utils.pbkdf2 import hmac_sha1 -#pkg -#local +# site +# pkg +from passlib.utils import classproperty, h64, safe_crypt, test_crypt +from passlib.utils.compat import b, bytes, u, uascii_to_str, unicode +from passlib.utils.pbkdf2 import get_prf +import passlib.utils.handlers as uh +# local __all__ = [ ] -#========================================================= -#sha1-crypt -#========================================================= +#============================================================================= +# sha1-crypt +#============================================================================= +_hmac_sha1 = get_prf("hmac-sha1")[0] +_BNULL = b('\x00') + class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the SHA1-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, an 8 character one will be autogenerated (this is recommended). If specified, it must be 0-64 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 8 bytes, but can be any value between 0 and 64. + :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 40000, must be between 1 and 4294967295, inclusive. - It will use the first available of two possible backends: - - * stdlib :func:`crypt()`, if the host OS supports sha1-crypt (NetBSD). - * a pure python implementation of sha1-crypt + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. - You can see which backend is in use by calling the :meth:`get_backend()` method. + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== #--GenericHandler-- name = "sha1_crypt" setting_kwds = ("salt", "salt_size", "rounds") - ident = u"$sha1$" + ident = u("$sha1$") checksum_size = 28 - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS #--HasSalt-- default_salt_size = 8 min_salt_size = 0 max_salt_size = 64 - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS #--HasRounds-- - default_rounds = 40000 #current passlib default - min_rounds = 1 #really, this should be higher. + default_rounds = 40000 # current passlib default + min_rounds = 1 # really, this should be higher. max_rounds = 4294967295 # 32-bit integer limit rounds_cost = "linear" - #========================================================= - #formatting - #========================================================= + #=================================================================== + # formatting + #=================================================================== @classmethod def from_string(cls, hash): - rounds, salt, chk = uh.parse_mc3(hash, cls.ident, cls.name) - if rounds.startswith("0"): - raise ValueError("invalid sha1-crypt hash (zero-padded rounds)") - return cls( - rounds=int(rounds), - salt=salt, - checksum=chk, - strict=bool(chk), - ) - - def to_string(self, native=True): - out = u"$sha1$%d$%s" % (self.rounds, self.salt) - if self.checksum: - out += u"$" + self.checksum - return to_hash_str(out) if native else out - - #========================================================= - #backend - #========================================================= + rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) + return cls(rounds=rounds, salt=salt, checksum=chk) + + def to_string(self, config=False): + chk = None if config else self.checksum + return uh.render_mc3(self.ident, self.rounds, self.salt, chk) + + #=================================================================== + # backend + #=================================================================== backends = ("os_crypt", "builtin") _has_backend_builtin = True @classproperty def _has_backend_os_crypt(cls): - h = u'$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHimExLaiSFlGkAe' - return bool(safe_os_crypt and safe_os_crypt(u"test",h)[1]==h) + return test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHim' + 'ExLaiSFlGkAe') def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") + if _BNULL in secret: + raise uh.exc.NullPasswordError(self) rounds = self.rounds - #NOTE: this uses a different format than the hash... - result = u"%s$sha1$%s" % (self.salt, rounds) - result = result.encode("ascii") + # NOTE: this seed value is NOT the same as the config string + result = (u("%s$sha1$%s") % (self.salt, rounds)).encode("ascii") + # NOTE: this algorithm is essentially PBKDF1, modified to use HMAC. r = 0 while r < rounds: - result = hmac_sha1(secret, result) + result = _hmac_sha1(secret, result) r += 1 return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii") @@ -131,16 +133,18 @@ ] def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.to_string(native=False)) - if ok: - return hash[hash.rindex("$")+1:] + config = self.to_string(config=True) + hash = safe_crypt(secret, config) + if hash: + assert hash.startswith(config) and len(hash) == len(config) + 29 + return hash[-28:] else: return self._calc_checksum_builtin(secret) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/sha2_crypt.py passlib-1.6.1/passlib/handlers/sha2_crypt.py --- passlib-1.5.3/passlib/handlers/sha2_crypt.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/sha2_crypt.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,382 +1,382 @@ -"""passlib.handlers.sha2_crypt - SHA256/512-CRYPT""" -#========================================================= -#imports -#========================================================= -#core -from hashlib import sha256, sha512 -import re +"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt""" +#============================================================================= +# imports +#============================================================================= +# core +import hashlib import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import h64, safe_os_crypt, classproperty, handlers as uh, \ - to_hash_str, to_unicode, bytes, b, bord -#pkg -#local +# site +# pkg +from passlib.utils import classproperty, h64, safe_crypt, test_crypt, \ + repeat_string, to_unicode +from passlib.utils.compat import b, bytes, byte_elem_value, irange, u, \ + uascii_to_str, unicode +import passlib.utils.handlers as uh +# local __all__ = [ - "SHA256Crypt", - "SHA512Crypt", + "sha512_crypt", + "sha256_crypt", ] -#========================================================= -#pure-python backend (shared between sha256-crypt & sha512-crypt) -#========================================================= -INVALID_SALT_VALUES = b("\x00$") - -def raw_sha_crypt(secret, salt, rounds, hash): - """perform raw sha crypt - - :arg secret: password to encode (if unicode, encoded to utf-8) - :arg salt: salt string to use (required) - :arg rounds: int rounds - :arg hash: hash constructor function for 256/512 variant - - :returns: - Returns tuple of ``(unencoded checksum, normalized salt, normalized rounds)``. - - """ - #validate secret - if not isinstance(secret, bytes): - raise TypeError("secret must be encoded as bytes") - - #validate rounds - if rounds < 1000: - rounds = 1000 - if rounds > 999999999: #pragma: no cover - rounds = 999999999 - - #validate salt - if not isinstance(salt, bytes): - raise TypeError("salt must be encoded as bytes") - if any(c in salt for c in INVALID_SALT_VALUES): - raise ValueError("invalid chars in salt") - if len(salt) > 16: - salt = salt[:16] - - #init helpers - def extend(source, size_ref): - "helper which repeats digest string until it's the same length as string" - assert len(source) == chunk_size - size = len(size_ref) - return source * int(size/chunk_size) + source[:size % chunk_size] - - #calc digest B - b = hash(secret) - chunk_size = b.digest_size #grab this once hash is created - b.update(salt) - a = b.copy() #make a copy to save a little time later - b.update(secret) - b_result = b.digest() - b_extend = extend(b_result, secret) - - #begin digest A - #a = hash(secret) <- performed above - #a.update(salt) <- performed above - a.update(b_extend) - - #for each bit in slen, add B or SECRET - value = len(secret) - while value > 0: - if value % 2: - a.update(b_result) - else: - a.update(secret) - value >>= 1 - - #finish A - a_result = a.digest() - - #calc DP - hash of password, extended to size of password - dp = hash(secret * len(secret)) - dp_result = extend(dp.digest(), secret) - - #calc DS - hash of salt, extended to size of salt - ds = hash(salt * (16+bord(a_result[0]))) - ds_result = extend(ds.digest(), salt) #aka 'S' - - # - #calc digest C - #NOTE: this has been contorted a little to allow pre-computing - #some of the hashes. the original algorithm was that - #each round generates digest composed of: - # if round%2>0 => dp else lr - # if round%3>0 => ds - # if round%7>0 => dp - # if round%2>0 => lr else dp - #where lr is digest of the last round's hash (initially = a_result) - # - - #pre-calculate some digests to speed up odd rounds - dp_hash = hash(dp_result).copy - dp_ds_hash = hash(dp_result + ds_result).copy - dp_dp_hash = hash(dp_result * 2).copy - dp_ds_dp_hash = hash(dp_result + ds_result + dp_result).copy - - #pre-calculate some strings to speed up even rounds - ds_dp_result = ds_result + dp_result - dp_dp_result = dp_result * 2 - ds_dp_dp_result = ds_result + dp_dp_result - - #run through rounds - last_result = a_result - i = 0 - while i < rounds: - if i % 2: - if i % 3: - if i % 7: - c = dp_ds_dp_hash() - else: - c = dp_ds_hash() - elif i % 7: - c = dp_dp_hash() - else: - c = dp_hash() - c.update(last_result) - else: - c = hash(last_result) - if i % 3: - if i % 7: - c.update(ds_dp_dp_result) - else: - c.update(ds_dp_result) - elif i % 7: - c.update(dp_dp_result) - else: - c.update(dp_result) - last_result = c.digest() - i += 1 - - #return unencoded result, along w/ normalized config values - return last_result, salt, rounds - -def raw_sha256_crypt(secret, salt, rounds): - "perform raw sha256-crypt; returns encoded checksum, normalized salt & rounds" - #run common crypt routine - result, salt, rounds = raw_sha_crypt(secret, salt, rounds, sha256) - out = h64.encode_transposed_bytes(result, _256_offsets) - assert len(out) == 43, "wrong length: %r" % (out,) - return out, salt, rounds - -_256_offsets = ( - 20, 10, 0, - 11, 1, 21, - 2, 22, 12, - 23, 13, 3, - 14, 4, 24, - 5, 25, 15, - 26, 16, 6, - 17, 7, 27, - 8, 28, 18, - 29, 19, 9, - 30, 31, +#============================================================================= +# pure-python backend, used by both sha256_crypt & sha512_crypt +# when crypt.crypt() backend is not available. +#============================================================================= +_BNULL = b('\x00') + +# pre-calculated offsets used to speed up C digest stage (see notes below). +# sequence generated using the following: + ##perms_order = "p,pp,ps,psp,sp,spp".split(",") + ##def offset(i): + ## key = (("p" if i % 2 else "") + ("s" if i % 3 else "") + + ## ("p" if i % 7 else "") + ("" if i % 2 else "p")) + ## return perms_order.index(key) + ##_c_digest_offsets = [(offset(i), offset(i+1)) for i in range(0,42,2)] +_c_digest_offsets = ( + (0, 3), (5, 1), (5, 3), (1, 2), (5, 1), (5, 3), (1, 3), + (4, 1), (5, 3), (1, 3), (5, 0), (5, 3), (1, 3), (5, 1), + (4, 3), (1, 3), (5, 1), (5, 2), (1, 3), (5, 1), (5, 3), + ) + +# map used to transpose bytes when encoding final sha256_crypt digest +_256_transpose_map = ( + 20, 10, 0, 11, 1, 21, 2, 22, 12, 23, 13, 3, 14, 4, 24, 5, + 25, 15, 26, 16, 6, 17, 7, 27, 8, 28, 18, 29, 19, 9, 30, 31, ) -def raw_sha512_crypt(secret, salt, rounds): - "perform raw sha512-crypt; returns encoded checksum, normalized salt & rounds" - #run common crypt routine - result, salt, rounds = raw_sha_crypt(secret, salt, rounds, sha512) - - ###encode result - out = h64.encode_transposed_bytes(result, _512_offsets) - assert len(out) == 86, "wrong length: %r" % (out,) - return out, salt, rounds - -_512_offsets = ( - 42, 21, 0, - 1, 43, 22, - 23, 2, 44, - 45, 24, 3, - 4, 46, 25, - 26, 5, 47, - 48, 27, 6, - 7, 49, 28, - 29, 8, 50, - 51, 30, 9, - 10, 52, 31, - 32, 11, 53, - 54, 33, 12, - 13, 55, 34, - 35, 14, 56, - 57, 36, 15, - 16, 58, 37, - 38, 17, 59, - 60, 39, 18, - 19, 61, 40, - 41, 20, 62, - 63, +# map used to transpose bytes when encoding final sha512_crypt digest +_512_transpose_map = ( + 42, 21, 0, 1, 43, 22, 23, 2, 44, 45, 24, 3, 4, 46, 25, 26, + 5, 47, 48, 27, 6, 7, 49, 28, 29, 8, 50, 51, 30, 9, 10, 52, + 31, 32, 11, 53, 54, 33, 12, 13, 55, 34, 35, 14, 56, 57, 36, 15, + 16, 58, 37, 38, 17, 59, 60, 39, 18, 19, 61, 40, 41, 20, 62, 63, ) -#========================================================= -#handler -#========================================================= -class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): - """This class implements the SHA256-Crypt password hash, and follows the :ref:`password-hash-api`. - - It supports a variable-length salt, and a variable number of rounds. - - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: - - :param salt: - Optional salt string. - If not specified, one will be autogenerated (this is recommended). - If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``. - - :param rounds: - Optional number of rounds to use. - Defaults to 40000, must be between 1000 and 999999999, inclusive. - - :param implicit_rounds: - this is an internal option which generally doesn't need to be touched. - - this flag determines whether the hash should omit the rounds parameter - when encoding it to a string; this is only permitted by the spec for rounds=5000, - and the flag is ignored otherwise. the spec requires the two different - encodings be preserved as they are, instead of normalizing them. - - It will use the first available of two possible backends: +def _raw_sha2_crypt(pwd, salt, rounds, use_512=False): + """perform raw sha256-crypt / sha512-crypt - * stdlib :func:`crypt()`, if the host OS supports SHA256-Crypt. - * a pure python implementation of SHA256-Crypt built into passlib. + this function provides a pure-python implementation of the internals + for the SHA256-Crypt and SHA512-Crypt algorithms; it doesn't + handle any of the parsing/validation of the hash strings themselves. + + :arg pwd: password chars/bytes to encrypt + :arg salt: salt chars to use + :arg rounds: linear rounds cost + :arg use_512: use sha512-crypt instead of sha256-crypt mode - You can see which backend is in use by calling the :meth:`get_backend()` method. + :returns: + encoded checksum chars """ + #=================================================================== + # init & validate inputs + #=================================================================== + + # validate secret + if isinstance(pwd, unicode): + # XXX: not sure what official unicode policy is, using this as default + pwd = pwd.encode("utf-8") + assert isinstance(pwd, bytes) + if _BNULL in pwd: + raise uh.exc.NullPasswordError(sha512_crypt if use_512 else sha256_crypt) + pwd_len = len(pwd) + + # validate rounds + assert 1000 <= rounds <= 999999999, "invalid rounds" + # NOTE: spec says out-of-range rounds should be clipped, instead of + # causing an error. this function assumes that's been taken care of + # by the handler class. + + # validate salt + assert isinstance(salt, unicode), "salt not unicode" + salt = salt.encode("ascii") + salt_len = len(salt) + assert salt_len < 17, "salt too large" + # NOTE: spec says salts larger than 16 bytes should be truncated, + # instead of causing an error. this function assumes that's been + # taken care of by the handler class. + + # load sha256/512 specific constants + if use_512: + hash_const = hashlib.sha512 + hash_len = 64 + transpose_map = _512_transpose_map + else: + hash_const = hashlib.sha256 + hash_len = 32 + transpose_map = _256_transpose_map + + #=================================================================== + # digest B - used as subinput to digest A + #=================================================================== + db = hash_const(pwd + salt + pwd).digest() + + #=================================================================== + # digest A - used to initialize first round of digest C + #=================================================================== + # start out with pwd + salt + a_ctx = hash_const(pwd + salt) + a_ctx_update = a_ctx.update + + # add pwd_len bytes of b, repeating b as many times as needed. + a_ctx_update(repeat_string(db, pwd_len)) + + # for each bit in pwd_len: add b if it's 1, or pwd if it's 0 + i = pwd_len + while i: + a_ctx_update(db if i & 1 else pwd) + i >>= 1 + + # finish A + da = a_ctx.digest() + + #=================================================================== + # digest P from password - used instead of password itself + # when calculating digest C. + #=================================================================== + if pwd_len < 64: + # method this is faster under python, but uses O(pwd_len**2) memory + # so we don't use it for larger passwords, to avoid a potential DOS. + dp = repeat_string(hash_const(pwd * pwd_len).digest(), pwd_len) + else: + tmp_ctx = hash_const(pwd) + tmp_ctx_update = tmp_ctx.update + i = pwd_len-1 + while i: + tmp_ctx_update(pwd) + i -= 1 + dp = repeat_string(tmp_ctx.digest(), pwd_len) + assert len(dp) == pwd_len + + #=================================================================== + # digest S - used instead of salt itself when calculating digest C + #=================================================================== + ds = hash_const(salt * (16 + byte_elem_value(da[0]))).digest()[:salt_len] + assert len(ds) == salt_len, "salt_len somehow > hash_len!" + + #=================================================================== + # digest C - for a variable number of rounds, combine A, S, and P + # digests in various ways; in order to burn CPU time. + #=================================================================== - #========================================================= - #algorithm information - #========================================================= - #--GenericHandler-- - name = "sha256_crypt" + # NOTE: the original SHA256/512-Crypt specification performs the C digest + # calculation using the following loop: + # + ##dc = da + ##i = 0 + ##while i < rounds: + ## tmp_ctx = hash_const(dp if i & 1 else dc) + ## if i % 3: + ## tmp_ctx.update(ds) + ## if i % 7: + ## tmp_ctx.update(dp) + ## tmp_ctx.update(dc if i & 1 else dp) + ## dc = tmp_ctx.digest() + ## i += 1 + # + # The code Passlib uses (below) implements an equivalent algorithm, + # it's just been heavily optimized to pre-calculate a large number + # of things beforehand. It works off of a couple of observations + # about the original algorithm: + # + # 1. each round is a combination of 'dc', 'ds', and 'dp'; determined + # by the whether 'i' a multiple of 2,3, and/or 7. + # 2. since lcm(2,3,7)==42, the series of combinations will repeat + # every 42 rounds. + # 3. even rounds 0-40 consist of 'hash(dc + round-specific-constant)'; + # while odd rounds 1-41 consist of hash(round-specific-constant + dc) + # + # Using these observations, the following code... + # * calculates the round-specific combination of ds & dp for each round 0-41 + # * runs through as many 42-round blocks as possible + # * runs through as many pairs of rounds as possible for remaining rounds + # * performs once last round if the total rounds should be odd. + # + # this cuts out a lot of the control overhead incurred when running the + # original loop 40,000+ times in python, resulting in ~20% increase in + # speed under CPython (though still 2x slower than glibc crypt) + + # prepare the 6 combinations of ds & dp which are needed + # (order of 'perms' must match how _c_digest_offsets was generated) + dp_dp = dp+dp + dp_ds = dp+ds + perms = [dp, dp_dp, dp_ds, dp_ds+dp, ds+dp, ds+dp_dp] + + # build up list of even-round & odd-round constants, + # and store in 21-element list as (even,odd) pairs. + data = [ (perms[even], perms[odd]) for even, odd in _c_digest_offsets] + + # perform as many full 42-round blocks as possible + dc = da + blocks, tail = divmod(rounds, 42) + while blocks: + for even, odd in data: + dc = hash_const(odd + hash_const(dc + even).digest()).digest() + blocks -= 1 + + # perform any leftover rounds + if tail: + # perform any pairs of rounds + pairs = tail>>1 + for even, odd in data[:pairs]: + dc = hash_const(odd + hash_const(dc + even).digest()).digest() + + # if rounds was odd, do one last round (since we started at 0, + # last round will be an even-numbered round) + if tail & 1: + dc = hash_const(dc + data[pairs][0]).digest() + + #=================================================================== + # encode digest using appropriate transpose map + #=================================================================== + return h64.encode_transposed_bytes(dc, transpose_map).decode("ascii") + +#============================================================================= +# handlers +#============================================================================= +_UROUNDS = u("rounds=") +_UDOLLAR = u("$") +_UZERO = u("0") + +class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, + uh.GenericHandler): + "class containing common code shared by sha256_crypt & sha512_crypt" + #=================================================================== + # class attrs + #=================================================================== + # name - set by subclass setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size") - ident = u"$5$" - checksum_chars = uh.H64_CHARS + # ident - set by subclass + checksum_chars = uh.HASH64_CHARS + # checksum_size - set by subclass - #--HasSalt-- min_salt_size = 0 max_salt_size = 16 - #TODO: allow salt charset 0-255 except for "\x00\n:$" - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - #--HasRounds-- - default_rounds = 40000 #current passlib default - min_rounds = 1000 #other bounds set by spec - max_rounds = 999999999 + min_rounds = 1000 # bounds set by spec + max_rounds = 999999999 # bounds set by spec rounds_cost = "linear" - #========================================================= - #init - #========================================================= + _cdb_use_512 = False # flag for _calc_digest_builtin() + _rounds_prefix = None # ident + _UROUNDS + + #=================================================================== + # methods + #=================================================================== + implicit_rounds = False + def __init__(self, implicit_rounds=None, **kwds): + super(_SHA2_Common, self).__init__(**kwds) + # if user calls encrypt() w/ 5000 rounds, default to compact form. if implicit_rounds is None: - implicit_rounds = True + implicit_rounds = (self.use_defaults and self.rounds == 5000) self.implicit_rounds = implicit_rounds - super(sha256_crypt, self).__init__(**kwds) - - #========================================================= - #parsing - #========================================================= - - #: regexp used to parse hashes - _pat = re.compile(ur""" - ^ - \$5 - (\$rounds=(?P\d+))? - \$ - ( - (?P[^:$]*) - | - (?P[^:$]{0,16}) - \$ - (?P[A-Za-z0-9./]{43})? - ) - $ - """, re.X) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._pat.match(hash) - if not m: - raise ValueError("invalid sha256-crypt hash") - rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk") - if rounds and rounds.startswith(u"0"): - raise ValueError("invalid sha256-crypt hash (zero-padded rounds)") + # basic format this parses - + # $5$[rounds=$][$] + + # TODO: this *could* use uh.parse_mc3(), except that the rounds + # portion has a slightly different grammar. + + # convert to unicode, check for ident prefix, split on dollar signs. + hash = to_unicode(hash, "ascii", "hash") + ident = cls.ident + if not hash.startswith(ident): + raise uh.exc.InvalidHashError(cls) + assert len(ident) == 3 + parts = hash[3:].split(_UDOLLAR) + + # extract rounds value + if parts[0].startswith(_UROUNDS): + assert len(_UROUNDS) == 7 + rounds = parts.pop(0)[7:] + if rounds.startswith(_UZERO) and rounds != _UZERO: + raise uh.exc.ZeroPaddedRoundsError(cls) + rounds = int(rounds) + implicit_rounds = False + else: + rounds = 5000 + implicit_rounds = True + + # rest should be salt and checksum + if len(parts) == 2: + salt, chk = parts + elif len(parts) == 1: + salt = parts[0] + chk = None + else: + raise uh.exc.MalformedHashError(cls) + + # return new object return cls( - implicit_rounds = not rounds, - rounds=int(rounds) if rounds else 5000, - salt=salt1 or salt2, - checksum=chk, - strict=bool(chk), - ) + rounds=rounds, + salt=salt, + checksum=chk or None, + implicit_rounds=implicit_rounds, + relaxed=not chk, # NOTE: relaxing parsing for config strings + # so that out-of-range rounds are clipped, + # since SHA2-Crypt spec treats them this way. + ) - def to_string(self, native=True): + def to_string(self): if self.rounds == 5000 and self.implicit_rounds: - hash = u"$5$%s$%s" % (self.salt, self.checksum or u'') + hash = u("%s%s$%s") % (self.ident, self.salt, + self.checksum or u('')) else: - hash = u"$5$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or u'') - return to_hash_str(hash) if native else hash - - #========================================================= - #backend - #========================================================= + hash = u("%srounds=%d$%s$%s") % (self.ident, self.rounds, + self.salt, self.checksum or u('')) + return uascii_to_str(hash) + + #=================================================================== + # backends + #=================================================================== backends = ("os_crypt", "builtin") _has_backend_builtin = True - @classproperty - def _has_backend_os_crypt(cls): - h = u"$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/" - return bool(safe_os_crypt and safe_os_crypt(u"test",h)[1]==h) + # _has_backend_os_crypt - provided by subclass def _calc_checksum_builtin(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - checksum, salt, rounds = raw_sha256_crypt(secret, - self.salt.encode("ascii"), - self.rounds) - assert salt == self.salt.encode("ascii"), \ - "class doesn't agree w/ builtin backend: salt %r != %r" % (salt, self.salt.encode("ascii")) - assert rounds == self.rounds, \ - "class doesn't agree w/ builtin backend: rounds %r != %r" % (rounds, self.rounds) - return checksum.decode("ascii") + return _raw_sha2_crypt(secret, self.salt, self.rounds, + self._cdb_use_512) def _calc_checksum_os_crypt(self, secret): - ok, result = safe_os_crypt(secret, self.to_string(native=False)) - if ok: - #NOTE: avoiding full parsing routine via from_string().checksum, + hash = safe_crypt(secret, self.to_string()) + if hash: + # NOTE: avoiding full parsing routine via from_string().checksum, # and just extracting the bit we need. - assert result.startswith(u"$5$") - chk = result[-43:] - assert u'$' not in chk - return chk + cs = self.checksum_size + assert hash.startswith(self.ident) and hash[-cs-1] == _UDOLLAR + return hash[-cs:] else: return self._calc_checksum_builtin(secret) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#sha 512 crypt -#========================================================= -class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler): - """This class implements the SHA512-Crypt password hash, and follows the :ref:`password-hash-api`. + #=================================================================== + # eoc + #=================================================================== + +class sha256_crypt(_SHA2_Common): + """This class implements the SHA256-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``. + :type rounds: int :param rounds: Optional number of rounds to use. - Defaults to 40000, must be between 1000 and 999999999, inclusive. + Defaults to 80000, must be between 1000 and 999999999, inclusive. + :type implicit_rounds: bool :param implicit_rounds: this is an internal option which generally doesn't need to be touched. @@ -385,133 +385,100 @@ and the flag is ignored otherwise. the spec requires the two different encodings be preserved as they are, instead of normalizing them. - It will use the first available of two possible backends: - - * stdlib :func:`crypt()`, if the host OS supports SHA512-Crypt. - * a pure python implementation of SHA512-Crypt built into passlib. + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. - You can see which backend is in use by calling the :meth:`get_backend()` method. + .. versionadded:: 1.6 """ + #=================================================================== + # class attrs + #=================================================================== + name = "sha256_crypt" + ident = u("$5$") + checksum_size = 43 + default_rounds = 80000 # current passlib default + + #=================================================================== + # backends + #=================================================================== + @classproperty + def _has_backend_os_crypt(cls): + return test_crypt("test", "$5$rounds=1000$test$QmQADEXMG8POI5W" + "Dsaeho0P36yK3Tcrgboabng6bkb/") - #========================================================= - #algorithm information - #========================================================= - name = "sha512_crypt" - ident = u"$6$" - checksum_chars = uh.H64_CHARS - - setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size") + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# sha 512 crypt +#============================================================================= +class sha512_crypt(_SHA2_Common): + """This class implements the SHA512-Crypt password hash, and follows the :ref:`password-hash-api`. - min_salt_size = 0 - max_salt_size = 16 - #TODO: allow salt charset 0-255 except for "\x00\n:$" - salt_chars = uh.H64_CHARS + It supports a variable-length salt, and a variable number of rounds. - default_rounds = 40000 #current passlib default - min_rounds = 1000 - max_rounds = 999999999 - rounds_cost = "linear" + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: - #========================================================= - #init - #========================================================= - def __init__(self, implicit_rounds=None, **kwds): - if implicit_rounds is None: - implicit_rounds = True - self.implicit_rounds = implicit_rounds - super(sha512_crypt, self).__init__(**kwds) + :type salt: str + :param salt: + Optional salt string. + If not specified, one will be autogenerated (this is recommended). + If specified, it must be 0-16 characters, drawn from the regexp range ``[./0-9A-Za-z]``. - #========================================================= - #parsing - #========================================================= - - #: regexp used to parse hashes - _pat = re.compile(ur""" - ^ - \$6 - (\$rounds=(?P\d+))? - \$ - ( - (?P[^:$\n]*) - | - (?P[^:$\n]{0,16}) - ( - \$ - (?P[A-Za-z0-9./]{86})? - )? - ) - $ - """, re.X) + :type rounds: int + :param rounds: + Optional number of rounds to use. + Defaults to 60000, must be between 1000 and 999999999, inclusive. - @classmethod - def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") - m = cls._pat.match(hash) - if not m: - raise ValueError("invalid sha512-crypt hash") - rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk") - if rounds and rounds.startswith("0"): - raise ValueError("invalid sha512-crypt hash (zero-padded rounds)") - return cls( - implicit_rounds = not rounds, - rounds=int(rounds) if rounds else 5000, - salt=salt1 or salt2, - checksum=chk, - strict=bool(chk), - ) + :type implicit_rounds: bool + :param implicit_rounds: + this is an internal option which generally doesn't need to be touched. - def to_string(self, native=True): - if self.rounds == 5000 and self.implicit_rounds: - hash = u"$6$%s$%s" % (self.salt, self.checksum or u'') - else: - hash = u"$6$rounds=%d$%s$%s" % (self.rounds, self.salt, self.checksum or u'') - return to_hash_str(hash) if native else hash + this flag determines whether the hash should omit the rounds parameter + when encoding it to a string; this is only permitted by the spec for rounds=5000, + and the flag is ignored otherwise. the spec requires the two different + encodings be preserved as they are, instead of normalizing them. - #========================================================= - #backend - #========================================================= - backends = ("os_crypt", "builtin") + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. - _has_backend_builtin = True + .. versionadded:: 1.6 + """ + #=================================================================== + # class attrs + #=================================================================== + name = "sha512_crypt" + ident = u("$6$") + checksum_size = 86 + _cdb_use_512 = True + default_rounds = 60000 # current passlib default + + #=================================================================== + # backend + #=================================================================== @classproperty def _has_backend_os_crypt(cls): - h = u"$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0" - return bool(safe_os_crypt and safe_os_crypt(u"test",h)[1]==h) - - #NOTE: testing w/ HashTimer shows 64-bit linux's crypt to be ~2.6x faster than builtin (627253 vs 238152 rounds/sec) - - def _calc_checksum_builtin(self, secret): - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - checksum, salt, rounds = raw_sha512_crypt(secret, - self.salt.encode("ascii"), - self.rounds) - assert salt == self.salt.encode("ascii"), \ - "class doesn't agree w/ builtin backend: salt %r != %r" % (salt, self.salt.encode("ascii")) - assert rounds == self.rounds, \ - "class doesn't agree w/ builtin backend: rounds %r != %r" % (rounds, self.rounds) - return checksum.decode("ascii") - - def _calc_checksum_os_crypt(self, secret): - ok, result = safe_os_crypt(secret, self.to_string(native=False)) - if ok: - #NOTE: avoiding full parsing routine via from_string().checksum, - # and just extracting the bit we need. - assert result.startswith(u"$6$") - chk = result[-86:] - assert u'$' not in chk - return chk - else: - return self._calc_checksum_builtin(secret) - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + return test_crypt("test", "$6$rounds=1000$test$2M/Lx6Mtobqj" + "Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn" + "yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG" + "I5c7TZauS0") + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/sun_md5_crypt.py passlib-1.6.1/passlib/handlers/sun_md5_crypt.py --- passlib-1.5.3/passlib/handlers/sun_md5_crypt.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/sun_md5_crypt.py 2012-08-01 17:10:03.000000000 +0000 @@ -7,27 +7,29 @@ See documentation for details. """ -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import h64, handlers as uh, to_hash_str, to_unicode, bytes, b, bord -#pkg -#local +# site +# pkg +from passlib.utils import h64, to_unicode +from passlib.utils.compat import b, bytes, byte_elem_value, irange, u, \ + uascii_to_str, unicode, str_to_bascii +import passlib.utils.handlers as uh +# local __all__ = [ "sun_md5_crypt", ] -#========================================================= -#backend -#========================================================= -#constant data used by alg - Hamlet act 3 scene 1 + null char +#============================================================================= +# backend +#============================================================================= +# constant data used by alg - Hamlet act 3 scene 1 + null char # exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt # from Project Gutenberg. @@ -69,13 +71,13 @@ "Be all my sins remember'd.\n\x00" #<- apparently null at end of C string is included (test vector won't pass otherwise) ) -#NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below -xr = range(7) +# NOTE: these sequences are pre-calculated iteration ranges used by X & Y loops w/in rounds function below +xr = irange(7) _XY_ROUNDS = [ - tuple((i,i,i+3) for i in xr), #xrounds 0 - tuple((i,i+1,i+4) for i in xr), #xrounds 1 - tuple((i,i+8,(i+11)&15) for i in xr), #yrounds 0 - tuple((i,(i+9)&15, (i+12)&15) for i in xr), #yrounds 1 + tuple((i,i,i+3) for i in xr), # xrounds 0 + tuple((i,i+1,i+4) for i in xr), # xrounds 1 + tuple((i,i+8,(i+11)&15) for i in xr), # yrounds 0 + tuple((i,(i+9)&15, (i+12)&15) for i in xr), # yrounds 1 ] del xr @@ -85,37 +87,45 @@ assert isinstance(secret, bytes) assert isinstance(salt, bytes) - #validate rounds + # validate rounds if rounds <= 0: rounds = 0 real_rounds = 4096 + rounds - #NOTE: spec seems to imply max 'rounds' is 2**32-1 + # NOTE: spec seems to imply max 'rounds' is 2**32-1 - #generate initial digest to start off round 0. - #NOTE: algorithm 'salt' includes full config string w/ trailing "$" + # generate initial digest to start off round 0. + # NOTE: algorithm 'salt' includes full config string w/ trailing "$" result = md5(secret + salt).digest() assert len(result) == 16 - #NOTE: many things have been inlined to speed up the loop as much as possible, - # so that this only barely resembles the algorithm as described in the docs. - # * all accesses to a given bit have been inlined using the formula - # rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1 - # * the calculation of coinflip value R has been inlined - # * the conditional division of coinflip value V has been inlined as a shift right of 0 or 1. - # * the i, i+3, etc iterations are precalculated in lists. - # * the round-based conditional division of x & y is now performed - # by choosing an appropriate precalculated list, so only the 7 used bits - # are actually calculated + # NOTE: many things in this function have been inlined (to speed up the loop + # as much as possible), to the point that this code barely resembles + # the algorithm as described in the docs. in particular: + # + # * all accesses to a given bit have been inlined using the formula + # rbitval(bit) = (rval((bit>>3) & 15) >> (bit & 7)) & 1 + # + # * the calculation of coinflip value R has been inlined + # + # * the conditional division of coinflip value V has been inlined as + # a shift right of 0 or 1. + # + # * the i, i+3, etc iterations are precalculated in lists. + # + # * the round-based conditional division of x & y is now performed + # by choosing an appropriate precalculated list, so that it only + # calculates the 7 bits which will actually be used. + # X_ROUNDS_0, X_ROUNDS_1, Y_ROUNDS_0, Y_ROUNDS_1 = _XY_ROUNDS - #NOTE: % appears to be *slightly* slower than &, so we prefer & if possible + # NOTE: % appears to be *slightly* slower than &, so we prefer & if possible round = 0 while round < real_rounds: - #convert last result byte string to list of byte-ints for easy access - rval = [ bord(c) for c in result ].__getitem__ + # convert last result byte string to list of byte-ints for easy access + rval = [ byte_elem_value(c) for c in result ].__getitem__ - #build up X bit by bit + # build up X bit by bit x = 0 xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0 for i, ia, ib in xrounds: @@ -124,7 +134,7 @@ v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1) x |= ((rval((v>>3)&15)>>(v&7))&1) << i - #build up Y bit by bit + # build up Y bit by bit y = 0 yrounds = Y_ROUNDS_1 if (rval(((round+64)>>3) & 15)>>(round & 7)) & 1 else Y_ROUNDS_0 for i, ia, ib in yrounds: @@ -133,10 +143,10 @@ v = rval((a >> (b % 5)) & 15) >> ((b>>(a&7)) & 1) y |= ((rval((v>>3)&15)>>(v&7))&1) << i - #extract x'th and y'th bit, xoring them together to yeild "coin flip" + # extract x'th and y'th bit, xoring them together to yeild "coin flip" coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1 - #construct hash for this round + # construct hash for this round h = md5(result) if coin: h.update(MAGIC_HAMLET) @@ -145,10 +155,10 @@ round += 1 - #encode output + # encode output return h64.encode_transposed_bytes(result, _chk_offsets) -#NOTE: same offsets as md5_crypt +# NOTE: same offsets as md5_crypt _chk_offsets = ( 12,6,0, 13,7,1, @@ -158,134 +168,148 @@ 11, ) -#========================================================= -#handler -#========================================================= +#============================================================================= +# handler +#============================================================================= class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): """This class implements the Sun-MD5-Crypt password hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. - The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: + :type salt: str :param salt: Optional salt string. If not specified, a salt will be autogenerated (this is recommended). If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``. + :type salt_size: int :param salt_size: If no salt is specified, this parameter can be used to specify the size (in characters) of the autogenerated salt. It currently defaults to 8. + :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 5000, must be between 0 and 4294963199, inclusive. + :type bare_salt: bool :param bare_salt: Optional flag used to enable an alternate salt digest behavior used by some hash strings in this scheme. This flag can be ignored by most users. Defaults to ``False``. (see :ref:`smc-bare-salt` for details). + + :type relaxed: bool + :param relaxed: + By default, providing an invalid value for one of the other + keywords will result in a :exc:`ValueError`. If ``relaxed=True``, + and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` + will be issued instead. Correctable errors include ``rounds`` + that are too small or too large, and ``salt`` strings that are too long. + + .. versionadded:: 1.6 """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== name = "sun_md5_crypt" setting_kwds = ("salt", "rounds", "bare_salt", "salt_size") - checksum_chars = uh.H64_CHARS + checksum_chars = uh.HASH64_CHARS + checksum_size = 22 - #NOTE: docs say max password length is 255. - #release 9u2 + # NOTE: docs say max password length is 255. + # release 9u2 - #NOTE: not sure if original crypt has a salt size limit, + # NOTE: not sure if original crypt has a salt size limit, # all instances that have been seen use 8 chars. default_salt_size = 8 min_salt_size = 0 max_salt_size = None - salt_chars = uh.H64_CHARS + salt_chars = uh.HASH64_CHARS - default_rounds = 5000 #current passlib default + default_rounds = 5000 # current passlib default min_rounds = 0 max_rounds = 4294963199 ##2**32-1-4096 - #XXX: ^ not sure what it does if past this bound... does 32 int roll over? + # XXX: ^ not sure what it does if past this bound... does 32 int roll over? rounds_cost = "linear" - _strict_rounds_bounds = True - #========================================================= - #instance attrs - #========================================================= - bare_salt = False #flag to indicate legacy hashes that lack "$$" suffix - - #========================================================= - #constructor - #========================================================= + ident_values = (u("$md5$"), u("$md5,")) + + #=================================================================== + # instance attrs + #=================================================================== + bare_salt = False # flag to indicate legacy hashes that lack "$$" suffix + + #=================================================================== + # constructor + #=================================================================== def __init__(self, bare_salt=False, **kwds): self.bare_salt = bare_salt super(sun_md5_crypt, self).__init__(**kwds) - #========================================================= - #internal helpers - #========================================================= + #=================================================================== + # internal helpers + #=================================================================== @classmethod def identify(cls, hash): - return uh.identify_prefix(hash, (u"$md5$", u"$md5,")) + hash = uh.to_unicode_for_identify(hash) + return hash.startswith(cls.ident_values) @classmethod def from_string(cls, hash): - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode("ascii") + hash = to_unicode(hash, "ascii", "hash") # - #detect if hash specifies rounds value. - #if so, parse and validate it. - #by end, set 'rounds' to int value, and 'tail' containing salt+chk + # detect if hash specifies rounds value. + # if so, parse and validate it. + # by end, set 'rounds' to int value, and 'tail' containing salt+chk # - if hash.startswith(u"$md5$"): + if hash.startswith(u("$md5$")): rounds = 0 salt_idx = 5 - elif hash.startswith(u"$md5,rounds="): - idx = hash.find(u"$", 12) + elif hash.startswith(u("$md5,rounds=")): + idx = hash.find(u("$"), 12) if idx == -1: - raise ValueError("invalid sun-md5-crypt hash (unexpected end of rounds)") + raise uh.exc.MalformedHashError(cls, "unexpected end of rounds") rstr = hash[12:idx] try: rounds = int(rstr) except ValueError: - raise ValueError("invalid sun-md5-crypt hash (bad rounds)") + raise uh.exc.MalformedHashError(cls, "bad rounds") if rstr != unicode(rounds): - raise ValueError("invalid sun-md5-crypt hash (zero-padded rounds)") + raise uh.exc.ZeroPaddedRoundsError(cls) if rounds == 0: - #NOTE: not sure if this is *forbidden* precisely, + # NOTE: not sure if this is forbidden by spec or not; # but allowing it would complicate things, # and it should never occur anyways. - raise ValueError("invalid sun-md5-crypt hash (explicit zero rounds)") + raise uh.exc.MalformedHashError(cls, "explicit zero rounds") salt_idx = idx+1 else: - raise ValueError("invalid sun-md5-crypt hash (unknown prefix)") + raise uh.exc.InvalidHashError(cls) # - #salt/checksum separation is kinda weird, - #to deal cleanly with some backward-compatible workarounds - #implemented by original implementation. + # salt/checksum separation is kinda weird, + # to deal cleanly with some backward-compatible workarounds + # implemented by original implementation. # - chk_idx = hash.rfind(u"$", salt_idx) + chk_idx = hash.rfind(u("$"), salt_idx) if chk_idx == -1: # ''-config for $-hash salt = hash[salt_idx:] chk = None bare_salt = True elif chk_idx == len(hash)-1: - if chk_idx > salt_idx and hash[-2] == u"$": - raise ValueError("invalid sun-md5-crypt hash (too many $)") + if chk_idx > salt_idx and hash[-2] == u("$"): + raise uh.exc.MalformedHashError(cls, "too many '$' separators") # $-config for $$-hash salt = hash[salt_idx:-1] chk = None bare_salt = False - elif chk_idx > 0 and hash[chk_idx-1] == u"$": + elif chk_idx > 0 and hash[chk_idx-1] == u("$"): # $$-hash salt = hash[salt_idx:chk_idx-1] chk = hash[chk_idx+1:] @@ -301,43 +325,40 @@ salt=salt, checksum=chk, bare_salt=bare_salt, - strict=bool(chk), ) - def to_string(self, withchk=True, native=True): - ss = u'' if self.bare_salt else u'$' + def to_string(self, withchk=True): + ss = u('') if self.bare_salt else u('$') rounds = self.rounds if rounds > 0: - out = u"$md5,rounds=%d$%s%s" % (rounds, self.salt, ss) + hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss) else: - out = u"$md5$%s%s" % (self.salt, ss) + hash = u("$md5$%s%s") % (self.salt, ss) if withchk: chk = self.checksum if chk: - out = u"%s$%s" % (out, chk) - return to_hash_str(out) if native else out + hash = u("%s$%s") % (hash, chk) + return uascii_to_str(hash) + + #=================================================================== + # primary interface + #=================================================================== + # TODO: if we're on solaris, check for native crypt() support. + # this will require extra testing, to make sure native crypt + # actually behaves correctly. of particular importance: + # when using ""-config, make sure to append "$x" to string. - #========================================================= - #primary interface - #========================================================= - #TODO: if we're on solaris, check for native crypt() support. - # this will require extra testing, to make sure native crypt - # actually behaves correctly. - # especially, when using ''-config, make sure to append '$x' to string. - - def calc_checksum(self, secret): - #NOTE: no reference for how sun_md5_crypt handles unicode - if secret is None: - raise TypeError("no secret specified") + def _calc_checksum(self, secret): + # NOTE: no reference for how sun_md5_crypt handles unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") - config = self.to_string(withchk=False,native=False).encode("ascii") + config = str_to_bascii(self.to_string(withchk=False)) return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii") - #========================================================= - #eoc - #========================================================= - -#========================================================= -#eof -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/handlers/windows.py passlib-1.6.1/passlib/handlers/windows.py --- passlib-1.5.3/passlib/handlers/windows.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/handlers/windows.py 2012-08-01 17:10:03.000000000 +0000 @@ -0,0 +1,310 @@ +"""passlib.handlers.nthash - Microsoft Windows -related hashes""" +#============================================================================= +# imports +#============================================================================= +# core +from binascii import hexlify +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +# site +# pkg +from passlib.utils import to_unicode, right_pad_string +from passlib.utils.compat import b, bytes, str_to_uascii, u, unicode, uascii_to_str +from passlib.utils.md4 import md4 +import passlib.utils.handlers as uh +# local +__all__ = [ + "lmhash", + "nthash", + "bsd_nthash", + "msdcc", + "msdcc2", +] + +#============================================================================= +# lanman hash +#============================================================================= +class lmhash(uh.HasEncodingContext, uh.StaticHandler): + """This class implements the Lan Manager Password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.verify` methods accept a single + optional keyword: + + :type encoding: str + :param encoding: + + This specifies what character encoding LMHASH should use when + calculating digest. It defaults to ``cp437``, the most + common encoding encountered. + + Note that while this class outputs digests in lower-case hexidecimal, + it will accept upper-case as well. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "lmhash" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + default_encoding = "cp437" + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret, self.encoding)).decode("ascii") + + # magic constant used by LMHASH + _magic = b("KGS!@#$%") + + @classmethod + def raw(cls, secret, encoding=None): + """encode password using LANMAN hash algorithm. + + :type secret: unicode or utf-8 encoded bytes + :arg secret: secret to hash + :type encoding: str + :arg encoding: + optional encoding to use for unicode inputs. + this defaults to ``cp437``, which is the + common case for most situations. + + :returns: returns string of raw bytes + """ + if not encoding: + encoding = cls.default_encoding + # some nice empircal data re: different encodings is at... + # http://www.openwall.com/lists/john-dev/2011/08/01/2 + # http://www.freerainbowtables.com/phpBB3/viewtopic.php?t=387&p=12163 + from passlib.utils.des import des_encrypt_block + MAGIC = cls._magic + if isinstance(secret, unicode): + # perform uppercasing while we're still unicode, + # to give a better shot at getting non-ascii chars right. + # (though some codepages do NOT upper-case the same as unicode). + secret = secret.upper().encode(encoding) + elif isinstance(secret, bytes): + # FIXME: just trusting ascii upper will work? + # and if not, how to do codepage specific case conversion? + # we could decode first using , + # but *that* might not always be right. + secret = secret.upper() + else: + raise TypeError("secret must be unicode or bytes") + secret = right_pad_string(secret, 14) + return des_encrypt_block(secret[0:7], MAGIC) + \ + des_encrypt_block(secret[7:14], MAGIC) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# ntlm hash +#============================================================================= +class nthash(uh.StaticHandler): + """This class implements the NT Password hash, and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + + Note that while this class outputs lower-case hexidecimal digests, + it will accept upper-case digests as well. + """ + #=================================================================== + # class attrs + #=================================================================== + name = "nthash" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + #=================================================================== + # methods + #=================================================================== + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret)).decode("ascii") + + @classmethod + def raw(cls, secret): + """encode password using MD4-based NTHASH algorithm + + :arg secret: secret as unicode or utf-8 encoded bytes + + :returns: returns string of raw bytes + """ + secret = to_unicode(secret, "utf-8", param="secret") + # XXX: found refs that say only first 128 chars are used. + return md4(secret.encode("utf-16-le")).digest() + + @classmethod + def raw_nthash(cls, secret, hex=False): + warn("nthash.raw_nthash() is deprecated, and will be removed " + "in Passlib 1.8, please use nthash.raw() instead", + DeprecationWarning) + ret = nthash.raw(secret) + return hexlify(ret).decode("ascii") if hex else ret + + #=================================================================== + # eoc + #=================================================================== + +bsd_nthash = uh.PrefixWrapper("bsd_nthash", nthash, prefix="$3$$", ident="$3$$", + doc="""The class support FreeBSD's representation of NTHASH + (which is compatible with the :ref:`modular-crypt-format`), + and follows the :ref:`password-hash-api`. + + It has no salt and a single fixed round. + + The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept no optional keywords. + """) + +##class ntlm_pair(object): +## "combined lmhash & nthash" +## name = "ntlm_pair" +## setting_kwds = () +## _hash_regex = re.compile(u"^(?P[0-9a-f]{32}):(?P[0-9][a-f]{32})$", +## re.I) +## +## @classmethod +## def identify(cls, hash): +## hash = to_unicode(hash, "latin-1", "hash") +## return len(hash) == 65 and cls._hash_regex.match(hash) is not None +## +## @classmethod +## def genconfig(cls): +## return None +## +## @classmethod +## def genhash(cls, secret, config): +## if config is not None and not cls.identify(config): +## raise uh.exc.InvalidHashError(cls) +## return cls.encrypt(secret) +## +## @classmethod +## def encrypt(cls, secret): +## return lmhash.encrypt(secret) + ":" + nthash.encrypt(secret) +## +## @classmethod +## def verify(cls, secret, hash): +## hash = to_unicode(hash, "ascii", "hash") +## m = cls._hash_regex.match(hash) +## if not m: +## raise uh.exc.InvalidHashError(cls) +## lm, nt = m.group("lm", "nt") +## # NOTE: verify against both in case encoding issue +## # causes one not to match. +## return lmhash.verify(secret, lm) or nthash.verify(secret, nt) + +#============================================================================= +# msdcc v1 +#============================================================================= +class msdcc(uh.HasUserContext, uh.StaticHandler): + """This class implements Microsoft's Domain Cached Credentials password hash, + and follows the :ref:`password-hash-api`. + + It has a fixed number of rounds, and uses the associated + username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods + have the following optional keywords: + + :type user: str + :param user: + String containing name of user account this password is associated with. + This is required to properly calculate the hash. + + This keyword is case-insensitive, and should contain just the username + (e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``). + + Note that while this class outputs lower-case hexidecimal digests, + it will accept upper-case digests as well. + """ + name = "msdcc" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret, self.user)).decode("ascii") + + @classmethod + def raw(cls, secret, user): + """encode password using mscash v1 algorithm + + :arg secret: secret as unicode or utf-8 encoded bytes + :arg user: username to use as salt + + :returns: returns string of raw bytes + """ + secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le") + user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le") + return md4(md4(secret).digest() + user).digest() + +#============================================================================= +# msdcc2 aka mscash2 +#============================================================================= +class msdcc2(uh.HasUserContext, uh.StaticHandler): + """This class implements version 2 of Microsoft's Domain Cached Credentials + password hash, and follows the :ref:`password-hash-api`. + + It has a fixed number of rounds, and uses the associated + username as the salt. + + The :meth:`~passlib.ifc.PasswordHash.encrypt`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods + have the following extra keyword: + + :type user: str + :param user: + String containing name of user account this password is associated with. + This is required to properly calculate the hash. + + This keyword is case-insensitive, and should contain just the username + (e.g. ``Administrator``, not ``SOMEDOMAIN\\Administrator``). + """ + name = "msdcc2" + checksum_chars = uh.HEX_CHARS + checksum_size = 32 + + @classmethod + def _norm_hash(cls, hash): + return hash.lower() + + def _calc_checksum(self, secret): + return hexlify(self.raw(secret, self.user)).decode("ascii") + + @classmethod + def raw(cls, secret, user): + """encode password using msdcc v2 algorithm + + :type secret: unicode or utf-8 bytes + :arg secret: secret + + :type user: str + :arg user: username to use as salt + + :returns: returns string of raw bytes + """ + from passlib.utils.pbkdf2 import pbkdf2 + secret = to_unicode(secret, "utf-8", param="secret").encode("utf-16-le") + user = to_unicode(user, "utf-8", param="user").lower().encode("utf-16-le") + tmp = md4(md4(secret).digest() + user).digest() + return pbkdf2(tmp, user, 10240, 16, 'hmac-sha1') + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/hash.py passlib-1.6.1/passlib/hash.py --- passlib-1.5.3/passlib/hash.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/hash.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,29 +1,28 @@ -"""passlib.hash stub +"""passlib.hash - proxy object mapping hash scheme names -> handlers -NOTE: - this module does not actually contain any hashes. - this file is a stub which is replaced by a proxy object, - which lazy-loads hashes as requested. +Note +==== +This module does not actually contain any hashes. This file +is a stub that replaces itself with a proxy object. - the actually implementations of hashes (at least, those built into passlib) - are stored in the passlib.handlers subpackage. +This proxy object (passlib.registry._PasslibRegistryProxy) +handles lazy-loading hashes as they are requested. + +The actual implementation of the various hashes is store elsewhere, +mainly in the submodules of the ``passlib.handlers`` package. """ -#NOTE: could support 'non-lazy' version which just imports -# all schemes known to list_crypt_handlers() +# NOTE: could support 'non-lazy' version which just imports +# all schemes known to list_crypt_handlers() + +#============================================================================= +# import proxy object and replace this module +#============================================================================= -#========================================================= -#import special proxy object as 'passlib.hash' module -#========================================================= - -#import proxy object, and replace this module with it. -#this should cause any import commands to return that object, -#not this module from passlib.registry import _proxy import sys -sys.modules['passlib.hash'] = _proxy -del sys, _proxy +sys.modules[__name__] = _proxy -#========================================================= -#eoc -#========================================================= +#============================================================================= +# eoc +#============================================================================= diff -Nru passlib-1.5.3/passlib/hosts.py passlib-1.6.1/passlib/hosts.py --- passlib-1.5.3/passlib/hosts.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/hosts.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,15 +1,16 @@ """passlib.hosts""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core import sys from warnings import warn -#pkg +# pkg from passlib.context import LazyCryptContext +from passlib.exc import PasslibRuntimeWarning from passlib.registry import get_crypt_handler -from passlib.utils import os_crypt, unix_crypt_schemes -#local +from passlib.utils import has_crypt, unix_crypt_schemes +# local __all__ = [ "linux_context", "linux2_context", "openbsd_context", @@ -18,23 +19,23 @@ "host_context", ] -#========================================================= -#linux support -#========================================================= +#============================================================================= +# linux support +#============================================================================= -#known platform names - linux2 +# known platform names - linux2 linux_context = linux2_context = LazyCryptContext( schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt", - "des_crypt", "unix_fallback" ], + "des_crypt", "unix_disabled" ], deprecated = [ "des_crypt" ], ) -#========================================================= -#bsd support -#========================================================= +#============================================================================= +# bsd support +#============================================================================= -#known platform names - +# known platform names - # freebsd2 # freebsd3 # freebsd4 @@ -44,22 +45,30 @@ # # netbsd1 -#referencing source via -http://fxr.googlebit.com -# freebsd 6,7,8 - des, md5, bcrypt, nthash +# referencing source via -http://fxr.googlebit.com +# freebsd 6,7,8 - des, md5, bcrypt, bsd_nthash # netbsd - des, ext, md5, bcrypt, sha1 # openbsd - des, ext, md5, bcrypt -freebsd_context = LazyCryptContext([ "bcrypt", "md5_crypt", "nthash", "des_crypt", "unix_fallback" ]) -openbsd_context = LazyCryptContext([ "bcrypt", "md5_crypt", "bsdi_crypt", "des_crypt", "unix_fallback" ]) -netbsd_context = LazyCryptContext([ "bcrypt", "sha1_crypt", "md5_crypt", "bsdi_crypt", "des_crypt", "unix_fallback" ]) - -#========================================================= -#current host -#========================================================= -if os_crypt: - #NOTE: this is basically mimicing the output of os crypt(), - #except that it uses passlib's (usually stronger) defaults settings, - #and can be introspected and used much more flexibly. +freebsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsd_nthash", + "des_crypt", "unix_disabled"]) + +openbsd_context = LazyCryptContext(["bcrypt", "md5_crypt", "bsdi_crypt", + "des_crypt", "unix_disabled"]) + +netbsd_context = LazyCryptContext(["bcrypt", "sha1_crypt", "md5_crypt", + "bsdi_crypt", "des_crypt", "unix_disabled"]) + +# XXX: include darwin in this list? it's got a BSD crypt variant, +# but that's not what it uses for user passwords. + +#============================================================================= +# current host +#============================================================================= +if has_crypt: + # NOTE: this is basically mimicing the output of os crypt(), + # except that it uses passlib's (usually stronger) defaults settings, + # and can be introspected and used much more flexibly. def _iter_os_crypt_schemes(): "helper which iterates over supported os_crypt schemes" @@ -70,36 +79,37 @@ found = True yield name if found: - #only offer fallback if there's another scheme in front, - #as this can't actually hash any passwords - yield "unix_fallback" - else: - #no idea what OS this could happen on, but just in case... - warn("crypt.crypt() function is present, but doesn't support any formats known to passlib!") + # only offer disabled handler if there's another scheme in front, + # as this can't actually hash any passwords + yield "unix_disabled" + else: # pragma: no cover -- sanity check + # no idea what OS this could happen on... + warn("crypt.crypt() function is present, but doesn't support any " + "formats known to passlib!", PasslibRuntimeWarning) host_context = LazyCryptContext(_iter_os_crypt_schemes()) -#========================================================= -#other platforms -#========================================================= - -#known platform strings - -#aix3 -#aix4 -#atheos -#beos5 -#darwin -#generic -#hp-ux11 -#irix5 -#irix6 -#mac -#next3 -#os2emx -#riscos -#sunos5 -#unixware7 - -#========================================================= -#eof -#========================================================= +#============================================================================= +# other platforms +#============================================================================= + +# known platform strings - +# aix3 +# aix4 +# atheos +# beos5 +# darwin +# generic +# hp-ux11 +# irix5 +# irix6 +# mac +# next3 +# os2emx +# riscos +# sunos5 +# unixware7 + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/ifc.py passlib-1.6.1/passlib/ifc.py --- passlib-1.5.3/passlib/ifc.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/ifc.py 2012-08-01 17:10:03.000000000 +0000 @@ -0,0 +1,193 @@ +"""passlib.ifc - abstract interfaces used by Passlib""" +#============================================================================= +# imports +#============================================================================= +# core +import logging; log = logging.getLogger(__name__) +import sys +# site +# pkg +# local +__all__ = [ + "PasswordHash", +] + +#============================================================================= +# 2.5-3.2 compatibility helpers +#============================================================================= +if sys.version_info >= (2,6): + from abc import ABCMeta, abstractmethod, abstractproperty +else: + # create stub for python 2.5 + ABCMeta = type + def abstractmethod(func): + return func +# def abstractproperty(): +# return None + +def create_with_metaclass(meta): + "class decorator that re-creates class using metaclass" + # have to do things this way since abc not present in py25, + # and py2/py3 have different ways of doing metaclasses. + def builder(cls): + if meta is type(cls): + return cls + return meta(cls.__name__, cls.__bases__, cls.__dict__.copy()) + return builder + +#============================================================================= +# PasswordHash interface +#============================================================================= +class PasswordHash(object): + """This class describes an abstract interface which all password hashes + in Passlib adhere to. Under Python 2.6 and up, this is an actual + Abstract Base Class built using the :mod:`!abc` module. + + See the Passlib docs for full documentation. + """ + #=================================================================== + # class attributes + #=================================================================== + + #--------------------------------------------------------------- + # general information + #--------------------------------------------------------------- + ##name + ##setting_kwds + ##context_kwds + + #--------------------------------------------------------------- + # salt information -- if 'salt' in setting_kwds + #--------------------------------------------------------------- + ##min_salt_size + ##max_salt_size + ##default_salt_size + ##salt_chars + ##default_salt_chars + + #--------------------------------------------------------------- + # rounds information -- if 'rounds' in setting_kwds + #--------------------------------------------------------------- + ##min_rounds + ##max_rounds + ##default_rounds + ##rounds_cost + + #--------------------------------------------------------------- + # encoding info -- if 'encoding' in context_kwds + #--------------------------------------------------------------- + ##default_encoding + + #=================================================================== + # primary methods + #=================================================================== + @classmethod + @abstractmethod + def encrypt(cls, secret, **setting_and_context_kwds): # pragma: no cover -- abstract method + "encrypt secret, returning resulting hash" + raise NotImplementedError("must be implemented by subclass") + + @classmethod + @abstractmethod + def verify(cls, secret, hash, **context_kwds): # pragma: no cover -- abstract method + "verify secret against hash, returns True/False" + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # additional methods + #=================================================================== + @classmethod + @abstractmethod + def identify(cls, hash): # pragma: no cover -- abstract method + "check if hash belongs to this scheme, returns True/False" + raise NotImplementedError("must be implemented by subclass") + + @classmethod + @abstractmethod + def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method + "compile settings into a configuration string for genhash()" + raise NotImplementedError("must be implemented by subclass") + + @classmethod + @abstractmethod + def genhash(cls, secret, config, **context_kwds): # pragma: no cover -- abstract method + "generated hash for secret, using settings from config/hash string" + raise NotImplementedError("must be implemented by subclass") + + #=================================================================== + # undocumented methods / attributes + #=================================================================== + # the following entry points are used internally by passlib, + # and aren't documented as part of the exposed interface. + # they are subject to change between releases, + # but are documented here so there's a list of them *somewhere*. + + #--------------------------------------------------------------- + # checksum information - defined for many hashes + #--------------------------------------------------------------- + ## checksum_chars + ## checksum_size + + #--------------------------------------------------------------- + # CryptContext flags + #--------------------------------------------------------------- + + # hack for bsdi_crypt: if True, causes CryptContext to only generate + # odd rounds values. assumed False if not defined. + ## _avoid_even_rounds = False + + ##@classmethod + ##def _bind_needs_update(cls, **setting_kwds): + ## """return helper to detect hashes that need updating. + ## + ## if this method is defined, the CryptContext constructor + ## will invoke it with the settings specified for the context. + ## this method should return either ``None``, or a callable + ## with the signature ``needs_update(hash,secret)->bool``. + ## + ## this ``needs_update`` function should return True if the hash + ## should be re-encrypted, whether due to internal + ## issues or the specified settings. + ## + ## CryptContext will automatically take care of deprecating + ## hashes with insufficient rounds for classes which define fromstring() + ## and a rounds attribute - though the requirements for this last + ## part may change at some point. + ## """ + + #--------------------------------------------------------------- + # experimental methods + #--------------------------------------------------------------- + + ##@classmethod + ##def normhash(cls, hash): + ## """helper to clean up non-canonic instances of hash. + ## currently only provided by bcrypt() to fix an historical passlib issue. + ## """ + + # experimental helper to parse hash into components. + ##@classmethod + ##def parsehash(cls, hash, checksum=True, sanitize=False): + ## """helper to parse hash into components, returns dict""" + + # experiment helper to estimate bitsize of different hashes, + # implement for GenericHandler, but may be currently be off for some hashes. + # want to expand this into a way to programmatically compare + # "strengths" of different hashes and hash algorithms. + # still needs to have some factor for estimate relative cost per round, + # ala in the style of the scrypt whitepaper. + ##@classmethod + ##def bitsize(cls, **kwds): + ## """returns dict mapping component -> bits contributed. + ## components currently include checksum, salt, rounds. + ## """ + + #=================================================================== + # eoc + #=================================================================== + +PasswordHash = create_with_metaclass(ABCMeta)(PasswordHash) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/registry.py passlib-1.6.1/passlib/registry.py --- passlib-1.5.3/passlib/registry.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/registry.py 2012-08-02 18:03:55.000000000 +0000 @@ -1,17 +1,15 @@ """passlib.registry - registry for password hash handlers""" -#========================================================= -#imports -#========================================================= -#core -import inspect +#============================================================================= +# imports +#============================================================================= +# core import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.utils import Undef, is_crypt_handler -#pkg -#local +# pkg +from passlib.exc import ExpectedTypeError, PasslibWarning +from passlib.utils import is_crypt_handler +# local __all__ = [ "register_crypt_handler_path", "register_crypt_handler", @@ -19,10 +17,10 @@ "list_crypt_handlers", ] -#========================================================= -#registry proxy object -#========================================================= -class PasslibRegistryProxy(object): +#============================================================================= +# proxy object used in place of 'passlib.hash' module +#============================================================================= +class _PasslibRegistryProxy(object): """proxy module passlib.hash this module is in fact an object which lazy-loads @@ -43,112 +41,150 @@ def __setattr__(self, attr, value): if attr.startswith("_"): - #NOTE: this is required for GAE, - # since it tries to set passlib.hash.__loader__ + # writing to private attributes should behave normally. + # (required so GAE can write to the __loader__ attribute). object.__setattr__(self, attr, value) else: - register_crypt_handler(value, name=attr) + # writing to public attributes should be treated + # as attempting to register a handler. + register_crypt_handler(value, _attr=attr) def __repr__(self): return "" def __dir__(self): - #add in handlers that will be lazy-loaded, - #otherwise this is std dir implementation + # this adds in lazy-loaded handler names, + # otherwise this is the standard dir() implementation. attrs = set(dir(self.__class__)) attrs.update(self.__dict__) - attrs.update(_handler_locations) + attrs.update(_locations) return sorted(attrs) - #========================================================= - #eoc - #========================================================= - -#singleton instance - available publicallly as 'passlib.hash' -_proxy = PasslibRegistryProxy() - -#========================================================== -#internal registry state -#========================================================== +# create single instance - available publically as 'passlib.hash' +_proxy = _PasslibRegistryProxy() -#: dict mapping name -> handler for all loaded handlers. uses proxy's dict so they stay in sync. +#============================================================================= +# internal registry state +#============================================================================= + +# singleton uses to detect omitted keywords +_UNSET = object() + +# dict mapping name -> loaded handlers (just uses proxy object's internal dict) _handlers = _proxy.__dict__ -#: dict mapping name -> (module path, attribute) for lazy-loading of handlers -_handler_locations = { - #NOTE: this is a hardcoded list of the handlers built into passlib, - #applications should call register_crypt_handler_location() to add their own - "apr_md5_crypt": ("passlib.handlers.md5_crypt", "apr_md5_crypt"), - "atlassian_pbkdf2_sha1": - ("passlib.handlers.pbkdf2", "atlassian_pbkdf2_sha1"), - "bcrypt": ("passlib.handlers.bcrypt", "bcrypt"), - "bigcrypt": ("passlib.handlers.des_crypt", "bigcrypt"), - "bsdi_crypt": ("passlib.handlers.des_crypt", "bsdi_crypt"), - "cta_pbkdf2_sha1": ("passlib.handlers.pbkdf2", "cta_pbkdf2_sha1"), - "crypt16": ("passlib.handlers.des_crypt", "crypt16"), - "des_crypt": ("passlib.handlers.des_crypt", "des_crypt"), - "django_salted_sha1": - ("passlib.handlers.django", "django_salted_sha1"), - "django_salted_md5":("passlib.handlers.django", "django_salted_md5"), - "django_des_crypt": ("passlib.handlers.django", "django_des_crypt"), - "django_disabled": ("passlib.handlers.django", "django_disabled"), - "dlitz_pbkdf2_sha1":("passlib.handlers.pbkdf2", "dlitz_pbkdf2_sha1"), - "fshp": ("passlib.handlers.fshp", "fshp"), - "grub_pbkdf2_sha512": - ("passlib.handlers.pbkdf2", "grub_pbkdf2_sha512"), - "hex_md4": ("passlib.handlers.digests", "hex_md4"), - "hex_md5": ("passlib.handlers.digests", "hex_md5"), - "hex_sha1": ("passlib.handlers.digests", "hex_sha1"), - "hex_sha256": ("passlib.handlers.digests", "hex_sha256"), - "hex_sha512": ("passlib.handlers.digests", "hex_sha512"), - "ldap_plaintext": ("passlib.handlers.ldap_digests","ldap_plaintext"), - "ldap_md5": ("passlib.handlers.ldap_digests","ldap_md5"), - "ldap_sha1": ("passlib.handlers.ldap_digests","ldap_sha1"), - "ldap_hex_md5": ("passlib.handlers.roundup", "ldap_hex_md5"), - "ldap_hex_sha1": ("passlib.handlers.roundup", "ldap_hex_sha1"), - "ldap_salted_md5": ("passlib.handlers.ldap_digests","ldap_salted_md5"), - "ldap_salted_sha1": ("passlib.handlers.ldap_digests","ldap_salted_sha1"), - "ldap_des_crypt": ("passlib.handlers.ldap_digests","ldap_des_crypt"), - "ldap_bsdi_crypt": ("passlib.handlers.ldap_digests","ldap_bsdi_crypt"), - "ldap_md5_crypt": ("passlib.handlers.ldap_digests","ldap_md5_crypt"), - "ldap_bcrypt": ("passlib.handlers.ldap_digests","ldap_bcrypt"), - "ldap_sha1_crypt": ("passlib.handlers.ldap_digests","ldap_sha1_crypt"), - "ldap_sha256_crypt":("passlib.handlers.ldap_digests","ldap_sha256_crypt"), - "ldap_sha512_crypt":("passlib.handlers.ldap_digests","ldap_sha512_crypt"), - "ldap_pbkdf2_sha1": ("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha1"), - "ldap_pbkdf2_sha256": - ("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha256"), - "ldap_pbkdf2_sha512": - ("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha512"), - "md5_crypt": ("passlib.handlers.md5_crypt", "md5_crypt"), - "mysql323": ("passlib.handlers.mysql", "mysql323"), - "mysql41": ("passlib.handlers.mysql", "mysql41"), - "nthash": ("passlib.handlers.nthash", "nthash"), - "oracle10": ("passlib.handlers.oracle", "oracle10"), - "oracle11": ("passlib.handlers.oracle", "oracle11"), - "pbkdf2_sha1": ("passlib.handlers.pbkdf2", "pbkdf2_sha1"), - "pbkdf2_sha256": ("passlib.handlers.pbkdf2", "pbkdf2_sha256"), - "pbkdf2_sha512": ("passlib.handlers.pbkdf2", "pbkdf2_sha512"), - "phpass": ("passlib.handlers.phpass", "phpass"), - "plaintext": ("passlib.handlers.misc", "plaintext"), - "postgres_md5": ("passlib.handlers.postgres", "postgres_md5"), - "roundup_plaintext":("passlib.handlers.roundup", "roundup_plaintext"), - "sha1_crypt": ("passlib.handlers.sha1_crypt", "sha1_crypt"), - "sha256_crypt": ("passlib.handlers.sha2_crypt", "sha256_crypt"), - "sha512_crypt": ("passlib.handlers.sha2_crypt", "sha512_crypt"), - "sun_md5_crypt": ("passlib.handlers.sun_md5_crypt","sun_md5_crypt"), - "unix_fallback": ("passlib.handlers.misc", "unix_fallback"), -} - -#: master regexp for detecting valid handler names -_name_re = re.compile("^[a-z][_a-z0-9]{2,}$") - -#: names which aren't allowed for various reasons (mainly keyword conflicts in CryptContext) -_forbidden_names = frozenset(["policy", "context", "all", "default", "none"]) - -#========================================================== -#registry frontend functions -#========================================================== +# dict mapping names -> import path for lazy loading. +# * import path should be "module.path" or "module.path:attr" +# * if attr omitted, "name" used as default. +_locations = dict( + # NOTE: this is a hardcoded list of the handlers built into passlib, + # applications should call register_crypt_handler_path() + apr_md5_crypt = "passlib.handlers.md5_crypt", + atlassian_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + bcrypt = "passlib.handlers.bcrypt", + bigcrypt = "passlib.handlers.des_crypt", + bsd_nthash = "passlib.handlers.windows", + bsdi_crypt = "passlib.handlers.des_crypt", + cisco_pix = "passlib.handlers.cisco", + cisco_type7 = "passlib.handlers.cisco", + cta_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + crypt16 = "passlib.handlers.des_crypt", + des_crypt = "passlib.handlers.des_crypt", + django_bcrypt = "passlib.handlers.django", + django_pbkdf2_sha256 = "passlib.handlers.django", + django_pbkdf2_sha1 = "passlib.handlers.django", + django_salted_sha1 = "passlib.handlers.django", + django_salted_md5 = "passlib.handlers.django", + django_des_crypt = "passlib.handlers.django", + django_disabled = "passlib.handlers.django", + dlitz_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + fshp = "passlib.handlers.fshp", + grub_pbkdf2_sha512 = "passlib.handlers.pbkdf2", + hex_md4 = "passlib.handlers.digests", + hex_md5 = "passlib.handlers.digests", + hex_sha1 = "passlib.handlers.digests", + hex_sha256 = "passlib.handlers.digests", + hex_sha512 = "passlib.handlers.digests", + htdigest = "passlib.handlers.digests", + ldap_plaintext = "passlib.handlers.ldap_digests", + ldap_md5 = "passlib.handlers.ldap_digests", + ldap_sha1 = "passlib.handlers.ldap_digests", + ldap_hex_md5 = "passlib.handlers.roundup", + ldap_hex_sha1 = "passlib.handlers.roundup", + ldap_salted_md5 = "passlib.handlers.ldap_digests", + ldap_salted_sha1 = "passlib.handlers.ldap_digests", + ldap_des_crypt = "passlib.handlers.ldap_digests", + ldap_bsdi_crypt = "passlib.handlers.ldap_digests", + ldap_md5_crypt = "passlib.handlers.ldap_digests", + ldap_bcrypt = "passlib.handlers.ldap_digests", + ldap_sha1_crypt = "passlib.handlers.ldap_digests", + ldap_sha256_crypt = "passlib.handlers.ldap_digests", + ldap_sha512_crypt = "passlib.handlers.ldap_digests", + ldap_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + ldap_pbkdf2_sha256 = "passlib.handlers.pbkdf2", + ldap_pbkdf2_sha512 = "passlib.handlers.pbkdf2", + lmhash = "passlib.handlers.windows", + md5_crypt = "passlib.handlers.md5_crypt", + msdcc = "passlib.handlers.windows", + msdcc2 = "passlib.handlers.windows", + mssql2000 = "passlib.handlers.mssql", + mssql2005 = "passlib.handlers.mssql", + mysql323 = "passlib.handlers.mysql", + mysql41 = "passlib.handlers.mysql", + nthash = "passlib.handlers.windows", + oracle10 = "passlib.handlers.oracle", + oracle11 = "passlib.handlers.oracle", + pbkdf2_sha1 = "passlib.handlers.pbkdf2", + pbkdf2_sha256 = "passlib.handlers.pbkdf2", + pbkdf2_sha512 = "passlib.handlers.pbkdf2", + phpass = "passlib.handlers.phpass", + plaintext = "passlib.handlers.misc", + postgres_md5 = "passlib.handlers.postgres", + roundup_plaintext = "passlib.handlers.roundup", + scram = "passlib.handlers.scram", + sha1_crypt = "passlib.handlers.sha1_crypt", + sha256_crypt = "passlib.handlers.sha2_crypt", + sha512_crypt = "passlib.handlers.sha2_crypt", + sun_md5_crypt = "passlib.handlers.sun_md5_crypt", + unix_disabled = "passlib.handlers.misc", + unix_fallback = "passlib.handlers.misc", +) + +# master regexp for detecting valid handler names +_name_re = re.compile("^[a-z][a-z0-9_]+[a-z0-9]$") + +# names which aren't allowed for various reasons +# (mainly keyword conflicts in CryptContext) +_forbidden_names = frozenset(["onload", "policy", "context", "all", + "default", "none", "auto"]) + +#============================================================================= +# registry frontend functions +#============================================================================= +def _validate_handler_name(name): + """helper to validate handler name + + :raises ValueError: + * if empty name + * if name not lower case + * if name contains double underscores + * if name is reserved (e.g. ``context``, ``all``). + """ + if not name: + raise ValueError("handler name cannot be empty: %r" % (name,)) + if name.lower() != name: + raise ValueError("name must be lower-case: %r" % (name,)) + if not _name_re.match(name): + raise ValueError("invalid name (must be 3+ characters, " + " begin with a-z, and contain only underscore, a-z, " + "0-9): %r" % (name,)) + if '__' in name: + raise ValueError("name may not contain double-underscores: %r" % + (name,)) + if name in _forbidden_names: + raise ValueError("that name is not allowed: %r" % (name,)) + return True + def register_crypt_handler_path(name, path): """register location to lazy-load handler when requested. @@ -175,14 +211,23 @@ >>> from passlib.registry import registry_crypt_handler_path >>> registry_crypt_handler_path("myhash", "myapp.helpers:MyHash") """ - global _handler_locations + # validate name + _validate_handler_name(name) + + # validate path + if path.startswith("."): + raise ValueError("path cannot start with '.'") if ':' in path: - modname, modattr = path.split(":") - else: - modname, modattr = path, name - _handler_locations[name] = (modname, modattr) + if path.count(':') > 1: + raise ValueError("path cannot have more than one ':'") + if path.find('.', path.index(':')) > -1: + raise ValueError("path cannot have '.' to right of ':'") + + # store location + _locations[name] = path + log.debug("registered path to %r handler: %r", name, path) -def register_crypt_handler(handler, force=False, name=None): +def register_crypt_handler(handler, force=False, _attr=None): """register password hash handler. this method immediately registers a handler with the internal passlib registry, @@ -190,7 +235,7 @@ :arg handler: the password hash handler to register :param force: force override of existing handler (defaults to False) - :param name: + :param _attr: [internal kwd] if specified, ensures ``handler.name`` matches this value, or raises :exc:`ValueError`. @@ -205,48 +250,36 @@ if a (different) handler was already registered with the same name, and ``force=True`` was not specified. """ - global _handlers, _name_re - - #validate handler + # validate handler if not is_crypt_handler(handler): - raise TypeError("object does not appear to be a crypt handler: %r" % (handler,)) - assert handler, "crypt handlers must be boolean True: %r" % (handler,) - - #if name specified, make sure it matched - #(this is mainly used as a check to help __setattr__) - if name: - if name != handler.name: - raise ValueError("handlers must be stored only under their own name") - else: - name = handler.name - - #validate name - if not name: - raise ValueError("name is null: %r" % (name,)) - if name.lower() != name: - raise ValueError("name must be lower-case: %r" % (name,)) - if not _name_re.match(name): - raise ValueError("invalid characters in name (must be 3+ characters, begin with a-z, and contain only underscore, a-z, 0-9): %r" % (name,)) - if '__' in name: - raise ValueError("name may not contain double-underscores: %r" % (name,)) - if name in _forbidden_names: - raise ValueError("that name is not allowed: %r" % (name,)) + raise ExpectedTypeError(handler, "password hash handler", "handler") + if not handler: + raise AssertionError("``bool(handler)`` must be True") + + # validate name + name = handler.name + _validate_handler_name(name) + if _attr and _attr != name: + raise ValueError("handlers must be stored only under their own name") - #check for existing handler + # check for existing handler other = _handlers.get(name) if other: if other is handler: - return #already registered - if force: - log.warning("overriding previous handler registered to name %r: %r", name, other) + log.debug("same %r handler already registered: %r", name, handler) + return + elif force: + log.warning("overriding previously registered %r handler: %r", + name, other) else: - raise KeyError("a handler has already registered for the name %r: %r (use force=True to override)" % (name, other)) + raise KeyError("another %r handler has already been registered: %r" % + (name, other)) - #register handler in dict + # register handler _handlers[name] = handler - log.info("registered crypt handler %r: %r", name, handler) + log.debug("registered %r handler: %r", name, handler) -def get_crypt_handler(name, default=Undef): +def get_crypt_handler(name, default=_UNSET): """return handler for specified password hash scheme. this method looks up a handler for the specified scheme. @@ -260,48 +293,63 @@ :returns: handler attached to name, or default value (if specified). """ - global _handlers, _handler_locations + # catch invalid names before we check _handlers, + # since it's a module dict, and exposes things like __package__, etc. + if name.startswith("_"): + if default is _UNSET: + raise KeyError("invalid handler name: %r" % (name,)) + else: + return default - #check if handler loaded - handler = _handlers.get(name) - if handler: - return handler + # check if handler is already loaded + try: + return _handlers[name] + except KeyError: + pass - #normalize name (and if changed, check dict again) + # normalize name (and if changed, check dict again) + assert isinstance(name, str), "name must be str instance" alt = name.replace("-","_").lower() if alt != name: - warn("handler names should be lower-case, and use underscores instead of hyphens: %r => %r" % (name, alt)) + warn("handler names should be lower-case, and use underscores instead " + "of hyphens: %r => %r" % (name, alt), PasslibWarning, + stacklevel=2) name = alt - #check if handler loaded - handler = _handlers.get(name) - if handler: - return handler + # try to load using new name + try: + return _handlers[name] + except KeyError: + pass + + # check if lazy load mapping has been specified for this driver + path = _locations.get(name) + if path: + if ':' in path: + modname, modattr = path.split(":") + else: + modname, modattr = path, name + ##log.debug("loading %r handler from path: '%s:%s'", name, modname, modattr) - #check if lazy load mapping has been specified for this driver - route = _handler_locations.get(name) - if route: - modname, modattr = route - - #try to load the module - any import errors indicate runtime config, - # either missing packages, or bad path provided to register_crypt_handler_path() - mod = __import__(modname, None, None, ['dummy'], 0) + # try to load the module - any import errors indicate runtime config, usually + # either missing package, or bad path provided to register_crypt_handler_path() + mod = __import__(modname, fromlist=[modattr], level=0) - #first check if importing module triggered register_crypt_handler(), - #(though this is discouraged due to it's magical implicitness) + # first check if importing module triggered register_crypt_handler(), + # (this is discouraged due to it's magical implicitness) handler = _handlers.get(name) if handler: - #XXX: issue deprecation warning here? + # XXX: issue deprecation warning here? assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler) return handler - #then get real handler & register it + # then get real handler & register it handler = getattr(mod, modattr) - register_crypt_handler(handler, name=name) + register_crypt_handler(handler, _attr=name) return handler - #fail! - if default is Undef: + # fail! + if default is _UNSET: raise KeyError("no crypt handler found for algorithm: %r" % (name,)) else: return default @@ -313,15 +361,16 @@ :returns: list of names of all known handlers """ - global _handlers, _handler_locations names = set(_handlers) if not loaded_only: - names.update(_handler_locations) - return sorted(names) + names.update(_locations) + # strip private attrs out of namespace and sort. + # TODO: make _handlers a separate list, so we don't have module namespace mixed in. + return sorted(name for name in names if not name.startswith("_")) -#NOTE: these two functions mainly exist just for the unittests... +# NOTE: these two functions mainly exist just for the unittests... -def has_crypt_handler(name, loaded_only=False): +def _has_crypt_handler(name, loaded_only=False): """check if handler name is known. this is only useful for two cases: @@ -332,8 +381,7 @@ :arg name: name of handler :param loaded_only: if ``True``, returns False if handler exists but hasn't been loaded """ - global _handlers, _handler_locations - return (name in _handlers) or (not loaded_only and name in _handler_locations) + return (name in _handlers) or (not loaded_only and name in _locations) def _unload_handler_name(name, locations=True): """unloads a handler from the registry. @@ -351,14 +399,11 @@ :arg name: name of handler to unload :param locations: if False, won't purge registered handler locations (default True) """ - global _handlers, _handler_locations - if name in _handlers: del _handlers[name] + if locations and name in _locations: + del _locations[name] - if locations and name in _handler_locations: - del _handler_locations[name] - -#========================================================= +#============================================================================= # eof -#========================================================= +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/__main__.py passlib-1.6.1/passlib/tests/__main__.py --- passlib-1.5.3/passlib/tests/__main__.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/__main__.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,5 +1,5 @@ import os -from nose import run +from nose import run run( defaultTest=os.path.dirname(__file__), ) diff -Nru passlib-1.5.3/passlib/tests/_test_bad_register.py passlib-1.6.1/passlib/tests/_test_bad_register.py --- passlib-1.5.3/passlib/tests/_test_bad_register.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/_test_bad_register.py 2012-08-01 17:10:03.000000000 +0000 @@ -8,8 +8,8 @@ class alt_dummy_bad(uh.StaticHandler): name = "dummy_bad" -#NOTE: if passlib.tests is being run from symlink (eg via gaeunit), -# this module may be imported a second time as test._test_bad_registry. -# we don't want it to do anything in that case. +# NOTE: if passlib.tests is being run from symlink (e.g. via gaeunit), +# this module may be imported a second time as test._test_bad_registry. +# we don't want it to do anything in that case. if __name__.startswith("passlib.tests"): register_crypt_handler(alt_dummy_bad) diff -Nru passlib-1.5.3/passlib/tests/backports.py passlib-1.6.1/passlib/tests/backports.py --- passlib-1.5.3/passlib/tests/backports.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/backports.py 2012-08-01 17:10:03.000000000 +0000 @@ -0,0 +1,329 @@ +"""backports of needed unittest2 features""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import logging; log = logging.getLogger(__name__) +import re +import sys +##from warnings import warn +# site +# pkg +from passlib.utils.compat import base_string_types +# local +__all__ = [ + "TestCase", + "skip", "skipIf", "skipUnless" + "catch_warnings", +] + +#============================================================================= +# import latest unittest module available +#============================================================================= +try: + import unittest2 as unittest + ut_version = 2 +except ImportError: + import unittest + if sys.version_info < (2,7) or (3,0) <= sys.version_info < (3,2): + # older versions of python will need to install the unittest2 + # backport (named unittest2_3k for 3.0/3.1) + ##warn("please install unittest2 for python %d.%d, it will be required " + ## "as of passlib 1.x" % sys.version_info[:2]) + ut_version = 1 + else: + ut_version = 2 + +#============================================================================= +# backport SkipTest support using nose +#============================================================================= +if ut_version < 2: + # used to provide replacement SkipTest() error + from nose.plugins.skip import SkipTest + + # hack up something to simulate skip() decorator + import functools + def skip(reason): + def decorator(test_item): + if isinstance(test_item, type) and issubclass(test_item, unittest.TestCase): + class skip_wrapper(test_item): + def setUp(self): + raise SkipTest(reason) + else: + @functools.wraps(test_item) + def skip_wrapper(*args, **kwargs): + raise SkipTest(reason) + return skip_wrapper + return decorator + + def skipIf(condition, reason): + if condition: + return skip(reason) + else: + return lambda item: item + + def skipUnless(condition, reason): + if condition: + return lambda item: item + else: + return skip(reason) + +else: + skip = unittest.skip + skipIf = unittest.skipIf + skipUnless = unittest.skipUnless + +#============================================================================= +# custom test harness +#============================================================================= +class TestCase(unittest.TestCase): + """backports a number of unittest2 features in TestCase""" + #=================================================================== + # backport some methods from unittest2 + #=================================================================== + if ut_version < 2: + + #---------------------------------------------------------------- + # simplistic backport of addCleanup() framework + #---------------------------------------------------------------- + _cleanups = None + + def addCleanup(self, function, *args, **kwds): + queue = self._cleanups + if queue is None: + queue = self._cleanups = [] + queue.append((function, args, kwds)) + + def doCleanups(self): + queue = self._cleanups + while queue: + func, args, kwds = queue.pop() + func(*args, **kwds) + + def tearDown(self): + self.doCleanups() + unittest.TestCase.tearDown(self) + + #---------------------------------------------------------------- + # backport skipTest (requires nose to work) + #---------------------------------------------------------------- + def skipTest(self, reason): + raise SkipTest(reason) + + #---------------------------------------------------------------- + # backport various assert tests added in unittest2 + #---------------------------------------------------------------- + def assertIs(self, real, correct, msg=None): + if real is not correct: + std = "got %r, expected would be %r" % (real, correct) + msg = self._formatMessage(msg, std) + raise self.failureException(msg) + + def assertIsNot(self, real, correct, msg=None): + if real is correct: + std = "got %r, expected would not be %r" % (real, correct) + msg = self._formatMessage(msg, std) + raise self.failureException(msg) + + def assertIsInstance(self, obj, klass, msg=None): + if not isinstance(obj, klass): + std = "got %r, expected instance of %r" % (obj, klass) + msg = self._formatMessage(msg, std) + raise self.failureException(msg) + + def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None): + """Fail if the two objects are unequal as determined by their + difference rounded to the given number of decimal places + (default 7) and comparing to zero, or by comparing that the + between the two objects is more than the given delta. + + Note that decimal places (from zero) are usually not the same + as significant digits (measured from the most signficant digit). + + If the two objects compare equal then they will automatically + compare almost equal. + """ + if first == second: + # shortcut + return + if delta is not None and places is not None: + raise TypeError("specify delta or places not both") + + if delta is not None: + if abs(first - second) <= delta: + return + + standardMsg = '%s != %s within %s delta' % (repr(first), + repr(second), + repr(delta)) + else: + if places is None: + places = 7 + + if round(abs(second-first), places) == 0: + return + + standardMsg = '%s != %s within %r places' % (repr(first), + repr(second), + places) + msg = self._formatMessage(msg, standardMsg) + raise self.failureException(msg) + + def assertLess(self, left, right, msg=None): + if left >= right: + std = "%r not less than %r" % (left, right) + raise self.failureException(self._formatMessage(msg, std)) + + def assertGreater(self, left, right, msg=None): + if left <= right: + std = "%r not greater than %r" % (left, right) + raise self.failureException(self._formatMessage(msg, std)) + + def assertGreaterEqual(self, left, right, msg=None): + if left < right: + std = "%r less than %r" % (left, right) + raise self.failureException(self._formatMessage(msg, std)) + + def assertIn(self, elem, container, msg=None): + if elem not in container: + std = "%r not found in %r" % (elem, container) + raise self.failureException(self._formatMessage(msg, std)) + + def assertNotIn(self, elem, container, msg=None): + if elem in container: + std = "%r unexpectedly in %r" % (elem, container) + raise self.failureException(self._formatMessage(msg, std)) + + #---------------------------------------------------------------- + # override some unittest1 methods to support _formatMessage + #---------------------------------------------------------------- + def assertEqual(self, real, correct, msg=None): + if real != correct: + std = "got %r, expected would equal %r" % (real, correct) + msg = self._formatMessage(msg, std) + raise self.failureException(msg) + + def assertNotEqual(self, real, correct, msg=None): + if real == correct: + std = "got %r, expected would not equal %r" % (real, correct) + msg = self._formatMessage(msg, std) + raise self.failureException(msg) + + #--------------------------------------------------------------- + # backport assertRegex() alias from 3.2 to 2.7/3.1 + #--------------------------------------------------------------- + if not hasattr(unittest.TestCase, "assertRegex"): + if hasattr(unittest.TestCase, "assertRegexpMatches"): + # was present in 2.7/3.1 under name assertRegexpMatches + assertRegex = unittest.TestCase.assertRegexpMatches + else: + # 3.0 and <= 2.6 didn't have this method at all + def assertRegex(self, text, expected_regex, msg=None): + """Fail the test unless the text matches the regular expression.""" + if isinstance(expected_regex, base_string_types): + assert expected_regex, "expected_regex must not be empty." + expected_regex = re.compile(expected_regex) + if not expected_regex.search(text): + msg = msg or "Regex didn't match: " + std = '%r not found in %r' % (msg, expected_regex.pattern, text) + raise self.failureException(self._formatMessage(msg, std)) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# backport catch_warnings +#============================================================================= +try: + from warnings import catch_warnings +except ImportError: + # catch_warnings wasn't added until py26. + # this adds backported copy from py26's stdlib + # so we can use it under py25. + + class WarningMessage(object): + + """Holds the result of a single showwarning() call.""" + + _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file", + "line") + + def __init__(self, message, category, filename, lineno, file=None, + line=None): + local_values = locals() + for attr in self._WARNING_DETAILS: + setattr(self, attr, local_values[attr]) + self._category_name = category.__name__ if category else None + + def __str__(self): + return ("{message : %r, category : %r, filename : %r, lineno : %s, " + "line : %r}" % (self.message, self._category_name, + self.filename, self.lineno, self.line)) + + + class catch_warnings(object): + + """A context manager that copies and restores the warnings filter upon + exiting the context. + + The 'record' argument specifies whether warnings should be captured by a + custom implementation of warnings.showwarning() and be appended to a list + returned by the context manager. Otherwise None is returned by the context + manager. The objects appended to the list are arguments whose attributes + mirror the arguments to showwarning(). + + The 'module' argument is to specify an alternative module to the module + named 'warnings' and imported under that name. This argument is only useful + when testing the warnings module itself. + + """ + + def __init__(self, record=False, module=None): + """Specify whether to record warnings and if an alternative module + should be used other than sys.modules['warnings']. + + For compatibility with Python 3.0, please consider all arguments to be + keyword-only. + + """ + self._record = record + self._module = sys.modules['warnings'] if module is None else module + self._entered = False + + def __repr__(self): + args = [] + if self._record: + args.append("record=True") + if self._module is not sys.modules['warnings']: + args.append("module=%r" % self._module) + name = type(self).__name__ + return "%s(%s)" % (name, ", ".join(args)) + + def __enter__(self): + if self._entered: + raise RuntimeError("Cannot enter %r twice" % self) + self._entered = True + self._filters = self._module.filters + self._module.filters = self._filters[:] + self._showwarning = self._module.showwarning + if self._record: + log = [] + def showwarning(*args, **kwargs): +# self._showwarning(*args, **kwargs) + log.append(WarningMessage(*args, **kwargs)) + self._module.showwarning = showwarning + return log + else: + return None + + def __exit__(self, *exc_info): + if not self._entered: + raise RuntimeError("Cannot exit %r without entering first" % self) + self._module.filters = self._filters + self._module.showwarning = self._showwarning + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/genconfig.py passlib-1.6.1/passlib/tests/genconfig.py --- passlib-1.5.3/passlib/tests/genconfig.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/genconfig.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,228 +0,0 @@ -"""passlib config generation script - -this script is a work in progress to develop a script which generates -rounds configuration parameters suitable for a particular host & deployment requirements. -right now it just consists of a function which experimentally determines -the optimal range of rounds values for a given hash, based on the desired time it should take. -""" -#========================================================= -#imports -#========================================================= -#core -from math import log as logb -import logging -import time -import sys -#site -#pkg -from passlib.registry import get_crypt_handler -#local -log = logging.getLogger(__name__) -#========================================================= -# -#========================================================= -class HashTimer(object): - """helper which determines number of rounds required for hash to take desired amount of time. - - usage:: - - >>> timer = HashTimer("sha512_crypt") - >>> timer.find_rounds(.5) - - .. note:: - This function is not very exact, and generates results - that are only approximately the same each time (w/in about 5% usually). - - Furthermore, to generate useful values, it should - be run when the system has an average load - to get an accurate measurement. - """ - log = logging.getLogger(__name__ + ".HashTimer") - - def __init__(self, name, samples=1): - # - #get handler, extract boundary information - # - self.handler = handler = get_crypt_handler(name) - if 'rounds' not in handler.setting_kwds: - raise ValueError("scheme does not support rounds: %r" % (handler.name,)) - self.min_rounds = getattr(handler, "min_rounds", 2) - self.max_rounds = getattr(handler, "max_rounds", (1<<32)-1) - rc = self.rounds_cost = getattr(handler, "rounds_cost", "linear") - - # - #set up functions that vary based on rounds cost function - # - if rc == "linear": - def get_rps(rounds, delta): - return rounds/delta - def guess_rounds(rps, target): - return int(rps*target+.5) - erradj = 2 - elif rc == "log2": - def get_rps(rounds, delta): - return (2**rounds)/delta - def guess_rounds(rps, target): - return int(logb(rps*target,2)+.5) - erradj = 1.1 - else: - raise NotImplementedError("unknown rounds cost function: %r" % (rc,)) - self.get_rps = get_rps - self.guess_rounds = guess_rounds - self.erradj = erradj - - # - #init cache - # - self.samples = samples - self.cache = {} - self.srange = range(samples) - - def time_encrypt(self, rounds): - "check how long encryption for a given number of rounds will take" - cache = self.cache - if rounds in cache: - return cache[rounds] - encrypt = self.handler.encrypt - srange = self.srange - cur = time.time - start = cur() - for x in srange: - encrypt("too many secrets", rounds=rounds) - stop = cur() - delta = (stop-start)/self.samples - cache[rounds] = delta - return delta - - def find_rounds(self, target, over=False, under=False): - """find optimal rounds range for hash - - :arg target: time hashing a password should take - :param over: if True, returns minimum rounds taking *at least* target seconds. - :param under: if True, returns maximum rounds taking *at most* target seconds. - - if neither over / under is set, returns rounds taking - closest to target seconds. - - :returns: - returns number of rounds closest - to taking about ``target`` seconds to hash a password. - """ - if target <= 0: - raise ValueError("target must be > 0") - - log = self.log - name = self.handler.name - get_rps = self.get_rps - time_encrypt = self.time_encrypt - log.info("%s: finding rounds for target time: %f", name, target) - - # - #check if useful lower & upper bounds already exist in cache - # - lower = upper = None - for rounds, delta in self.cache.iteritems(): - if delta < target: - if lower is None or rounds > lower: - lower = rounds - else: - if upper is None or rounds < upper: - upper = rounds - - # - #if bounds not found in cache, run open-ended search for starting bounds - # - if lower is None: - lower = max(1,self.min_rounds) - if upper is None: - guess_rounds = self.guess_rounds - max_rounds = self.max_rounds - target_above = target*self.erradj #NOTE: we aim a little high as hack to deal w/ measuring error - rounds = lower - while True: - delta = time_encrypt(rounds) - rps = get_rps(rounds, delta) - log.debug("%s: ranging target: checked %r -> %fs (%f r/s)", name, rounds, delta, rps) - if delta < target: - lower = rounds - if rounds == max_rounds: - log.warning("%s: target time out of range: hash would require > max_rounds (%d) in order to take %fs", name, max_rounds, target) - return rounds - rounds = min(max(guess_rounds(rps, target_above), rounds+1), max_rounds) - else: - upper = rounds - break - - # - #perform binary search till we find match - # - while lower+1 %fs (%f r/s)", name, lower, upper, next, delta, rps) - if delta < target: - lower = next - else: - upper = next - - # - #now 'lower' is largest value which takes less than target seconds, - #and 'upper' is smallest value which takes greater than target seconds. - #so we pick based on over/under flags, or fallback to whichever one is closest - # - if over: - return upper - elif under: - return lower - else: - if target-cache[lower] < cache[upper]-target: - return lower - else: - return upper - - def find_rounds_range(self, target_high, target_low=None, over=False, under=False): - "find min/max rounds which will cause scheme to take specified range of times" - if target_low is None: - target_low = target_high * .75 - elif target_low > target_high: - target_high = target_low - rounds_high = self.find_rounds(target_high, under=not over, over=over) - rounds_low = self.find_rounds(target_low, over=not under, under=under) - if rounds_low > rounds_high: - #NOTE: this happens sometimes w/ rounds_cost=log2... - #if nothing hits w/in range, rounds_low will be 1+ rounds_high - #we just return correctly ordered range - rounds_low, rounds_high = rounds_high, rounds_low - return rounds_low, rounds_high - - def estimate_rps(self): - "return estimated rounds per second based on cached results" - cache = self.cache - if not cache: - raise RuntimeError("should not be called until cache populated by find_rounds()") - get_rps = self.get_rps - rps = sum(r*get_rps(r,d) for r,d in cache.iteritems())/sum(cache) - if rps > 1000: #for almost all cases, we'd return integer - rps = int(rps) - return rps - -#========================================================= -#main -#========================================================= -def main(*args): - from bps.logs import setup_std_logging - setup_std_logging(level="debug", dev=True) - - timer = HashTimer("sha256_crypt") - print timer.find_rounds_range(.5) - print timer.estimate_rps() - - #TODO: give script ability to generate timings for a range of schemes, and minimum / maximum times. - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) -#========================================================= -#eof -#========================================================= diff -Nru passlib-1.5.3/passlib/tests/sample1.cfg passlib-1.6.1/passlib/tests/sample1.cfg --- passlib-1.5.3/passlib/tests/sample1.cfg 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/sample1.cfg 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,9 @@ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all__vary_rounds = 0.1 +bsdi_crypt__default_rounds = 25000 +bsdi_crypt__max_rounds = 30000 +sha512_crypt__max_rounds = 50000 +sha512_crypt__min_rounds = 40000 + diff -Nru passlib-1.5.3/passlib/tests/sample1b.cfg passlib-1.6.1/passlib/tests/sample1b.cfg --- passlib-1.5.3/passlib/tests/sample1b.cfg 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/sample1b.cfg 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,9 @@ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all__vary_rounds = 0.1 +bsdi_crypt__default_rounds = 25000 +bsdi_crypt__max_rounds = 30000 +sha512_crypt__max_rounds = 50000 +sha512_crypt__min_rounds = 40000 + Binary files /tmp/P8vzqGfgxa/passlib-1.5.3/passlib/tests/sample1c.cfg and /tmp/wW008SfOkD/passlib-1.6.1/passlib/tests/sample1c.cfg differ diff -Nru passlib-1.5.3/passlib/tests/test_apache.py passlib-1.6.1/passlib/tests/test_apache.py --- passlib-1.5.3/passlib/tests/test_apache.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_apache.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,40 +1,54 @@ """tests for passlib.apache -- (c) Assurance Technologies 2008-2011""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import hashlib from logging import getLogger import os import time -#site -#pkg +# site +# pkg from passlib import apache -from passlib.utils import b, native_str, bytes -from passlib.tests.utils import TestCase, mktemp, gae_env, get_file, set_file -#module +from passlib.utils.compat import irange, unicode +from passlib.tests.utils import TestCase, get_file, set_file, catch_warnings, ensure_mtime_changed +from passlib.utils.compat import b, bytes, u +# module log = getLogger(__name__) def backdate_file_mtime(path, offset=10): "backdate file's mtime by specified amount" - #NOTE: this is used so we can test code which detects mtime changes, - # without having to actually *pause* for that long. + # NOTE: this is used so we can test code which detects mtime changes, + # without having to actually *pause* for that long. atime = os.path.getatime(path) mtime = os.path.getmtime(path)-offset os.utime(path, (atime, mtime)) -#========================================================= -#htpasswd -#========================================================= +#============================================================================= +# htpasswd +#============================================================================= class HtpasswdFileTest(TestCase): "test HtpasswdFile class" - case_prefix = "HtpasswdFile" + descriptionPrefix = "HtpasswdFile" - sample_01 = b('user2:2CHkkwa2AtqGs\nuser3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\nuser1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n') + # sample with 4 users + sample_01 = b('user2:2CHkkwa2AtqGs\n' + 'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n' + 'user4:pass4\n' + 'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n') + + # sample 1 with user 1, 2 deleted; 4 changed sample_02 = b('user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\n') - sample_03 = b('user2:pass2x\nuser3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\nuser1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\nuser5:pass5\n') + # sample 1 with user2 updated, user 1 first entry removed, and user 5 added + sample_03 = b('user2:pass2x\n' + 'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n' + 'user4:pass4\n' + 'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n' + 'user5:pass5\n') + + # standalone sample with 8-bit username sample_04_utf8 = b('user\xc3\xa6:2CHkkwa2AtqGs\n') sample_04_latin1 = b('user\xe6:2CHkkwa2AtqGs\n') @@ -42,294 +56,455 @@ def test_00_constructor_autoload(self): "test constructor autoload" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - #check with existing file - path = mktemp() + # check with existing file + path = self.mktemp() set_file(path, self.sample_01) ht = apache.HtpasswdFile(path) self.assertEqual(ht.to_string(), self.sample_01) + self.assertEqual(ht.path, path) + self.assertTrue(ht.mtime) + + # check changing path + ht.path = path + "x" + self.assertEqual(ht.path, path + "x") + self.assertFalse(ht.mtime) - #check autoload=False - ht = apache.HtpasswdFile(path, autoload=False) + # check new=True + ht = apache.HtpasswdFile(path, new=True) self.assertEqual(ht.to_string(), b("")) + self.assertEqual(ht.path, path) + self.assertFalse(ht.mtime) - #check missing file + # check autoload=False (deprecated alias for new=True) + with self.assertWarningList("``autoload=False`` is deprecated"): + ht = apache.HtpasswdFile(path, autoload=False) + self.assertEqual(ht.to_string(), b("")) + self.assertEqual(ht.path, path) + self.assertFalse(ht.mtime) + + # check missing file os.remove(path) self.assertRaises(IOError, apache.HtpasswdFile, path) - #NOTE: "default" option checked via update() test, among others + # NOTE: "default_scheme" option checked via set_password() test, among others + + def test_00_from_path(self): + path = self.mktemp() + set_file(path, self.sample_01) + ht = apache.HtpasswdFile.from_path(path) + self.assertEqual(ht.to_string(), self.sample_01) + self.assertEqual(ht.path, None) + self.assertFalse(ht.mtime) def test_01_delete(self): "test delete()" - ht = apache.HtpasswdFile._from_string(self.sample_01) - self.assertTrue(ht.delete("user1")) + ht = apache.HtpasswdFile.from_string(self.sample_01) + self.assertTrue(ht.delete("user1")) # should delete both entries self.assertTrue(ht.delete("user2")) - self.assertTrue(not ht.delete("user5")) + self.assertFalse(ht.delete("user5")) # user not present self.assertEqual(ht.to_string(), self.sample_02) + # invalid user self.assertRaises(ValueError, ht.delete, "user:") - def test_02_update(self): - "test update()" - ht = apache.HtpasswdFile._from_string( - self.sample_01, default="plaintext") - self.assertTrue(ht.update("user2", "pass2x")) - self.assertTrue(not ht.update("user5", "pass5")) + def test_01_delete_autosave(self): + path = self.mktemp() + sample = b('user1:pass1\nuser2:pass2\n') + set_file(path, sample) + + ht = apache.HtpasswdFile(path) + ht.delete("user1") + self.assertEqual(get_file(path), sample) + + ht = apache.HtpasswdFile(path, autosave=True) + ht.delete("user1") + self.assertEqual(get_file(path), b("user2:pass2\n")) + + def test_02_set_password(self): + "test set_password()" + ht = apache.HtpasswdFile.from_string( + self.sample_01, default_scheme="plaintext") + self.assertTrue(ht.set_password("user2", "pass2x")) + self.assertFalse(ht.set_password("user5", "pass5")) self.assertEqual(ht.to_string(), self.sample_03) - self.assertRaises(ValueError, ht.update, "user:", "pass") + # test legacy default kwd + with self.assertWarningList("``default`` is deprecated"): + ht = apache.HtpasswdFile.from_string(self.sample_01, default="plaintext") + self.assertTrue(ht.set_password("user2", "pass2x")) + self.assertFalse(ht.set_password("user5", "pass5")) + self.assertEqual(ht.to_string(), self.sample_03) + + # invalid user + self.assertRaises(ValueError, ht.set_password, "user:", "pass") + + # test that legacy update() still works + with self.assertWarningList("update\(\) is deprecated"): + ht.update("user2", "test") + self.assertTrue(ht.check_password("user2", "test")) + + def test_02_set_password_autosave(self): + path = self.mktemp() + sample = b('user1:pass1\n') + set_file(path, sample) + + ht = apache.HtpasswdFile(path) + ht.set_password("user1", "pass2") + self.assertEqual(get_file(path), sample) + + ht = apache.HtpasswdFile(path, default_scheme="plaintext", autosave=True) + ht.set_password("user1", "pass2") + self.assertEqual(get_file(path), b("user1:pass2\n")) def test_03_users(self): "test users()" - ht = apache.HtpasswdFile._from_string(self.sample_01) - ht.update("user5", "pass5") + ht = apache.HtpasswdFile.from_string(self.sample_01) + ht.set_password("user5", "pass5") ht.delete("user3") - ht.update("user3", "pass3") - self.assertEqual(ht.users(), ["user2", "user4", "user1", "user5", "user3"]) - - def test_04_verify(self): - "test verify()" - ht = apache.HtpasswdFile._from_string(self.sample_01) - self.assertTrue(ht.verify("user5","pass5") is None) - for i in xrange(1,5): + ht.set_password("user3", "pass3") + self.assertEqual(ht.users(), ["user2", "user4", "user1", "user5", + "user3"]) + + def test_04_check_password(self): + "test check_password()" + ht = apache.HtpasswdFile.from_string(self.sample_01) + self.assertRaises(TypeError, ht.check_password, 1, 'pass5') + self.assertTrue(ht.check_password("user5","pass5") is None) + for i in irange(1,5): i = str(i) - self.assertTrue(ht.verify("user"+i, "pass"+i)) - self.assertTrue(ht.verify("user"+i, "pass5") is False) + self.assertTrue(ht.check_password("user"+i, "pass"+i)) + self.assertTrue(ht.check_password("user"+i, "pass5") is False) - self.assertRaises(ValueError, ht.verify, "user:", "pass") + self.assertRaises(ValueError, ht.check_password, "user:", "pass") + + # test that legacy verify() still works + with self.assertWarningList(["verify\(\) is deprecated"]*2): + self.assertTrue(ht.verify("user1", "pass1")) + self.assertFalse(ht.verify("user1", "pass2")) def test_05_load(self): "test load()" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - #setup empty file - path = mktemp() + # setup empty file + path = self.mktemp() set_file(path, "") backdate_file_mtime(path, 5) - ha = apache.HtpasswdFile(path, default="plaintext") + ha = apache.HtpasswdFile(path, default_scheme="plaintext") self.assertEqual(ha.to_string(), b("")) - #make changes, check force=False does nothing - ha.update("user1", "pass1") - ha.load(force=False) + # make changes, check load_if_changed() does nothing + ha.set_password("user1", "pass1") + ha.load_if_changed() self.assertEqual(ha.to_string(), b("user1:pass1\n")) - #change file + # change file set_file(path, self.sample_01) - ha.load(force=False) + ha.load_if_changed() self.assertEqual(ha.to_string(), self.sample_01) - #make changes, check force=True overwrites them - ha.update("user5", "pass5") + # make changes, check load() overwrites them + ha.set_password("user5", "pass5") ha.load() self.assertEqual(ha.to_string(), self.sample_01) - #test load w/ no path + # test load w/ no path hb = apache.HtpasswdFile() self.assertRaises(RuntimeError, hb.load) - self.assertRaises(RuntimeError, hb.load, force=False) + self.assertRaises(RuntimeError, hb.load_if_changed) - #test load w/ dups + # test load w/ dups and explicit path set_file(path, self.sample_dup) - hc = apache.HtpasswdFile(path) - self.assertTrue(hc.verify('user1','pass1')) + hc = apache.HtpasswdFile() + hc.load(path) + self.assertTrue(hc.check_password('user1','pass1')) + + # NOTE: load_string() tested via from_string(), which is used all over this file def test_06_save(self): "test save()" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - #load from file - path = mktemp() + # load from file + path = self.mktemp() set_file(path, self.sample_01) ht = apache.HtpasswdFile(path) - #make changes, check they saved + # make changes, check they saved ht.delete("user1") ht.delete("user2") ht.save() self.assertEqual(get_file(path), self.sample_02) - #test save w/ no path - hb = apache.HtpasswdFile() - hb.update("user1", "pass1") + # test save w/ no path + hb = apache.HtpasswdFile(default_scheme="plaintext") + hb.set_password("user1", "pass1") self.assertRaises(RuntimeError, hb.save) + # test save w/ explicit path + hb.save(path) + self.assertEqual(get_file(path), b("user1:pass1\n")) + def test_07_encodings(self): - "test encoding parameter behavior" - #test bad encodings cause failure in constructor + "test 'encoding' kwd" + # test bad encodings cause failure in constructor self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16") - #check users() returns native string by default - ht = apache.HtpasswdFile._from_string(self.sample_01) - self.assertIsInstance(ht.users()[0], native_str) - - #check returns unicode if encoding explicitly set - ht = apache.HtpasswdFile._from_string(self.sample_01, encoding="utf-8") - self.assertIsInstance(ht.users()[0], unicode) - - #check returns bytes if encoding explicitly disabled - ht = apache.HtpasswdFile._from_string(self.sample_01, encoding=None) - self.assertIsInstance(ht.users()[0], bytes) - - #check sample utf-8 - ht = apache.HtpasswdFile._from_string(self.sample_04_utf8, encoding="utf-8") - self.assertEqual(ht.users(), [ u"user\u00e6" ]) - - #check sample latin-1 - ht = apache.HtpasswdFile._from_string(self.sample_04_latin1, - encoding="latin-1") - self.assertEqual(ht.users(), [ u"user\u00e6" ]) + # check sample utf-8 + ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding="utf-8", + return_unicode=True) + self.assertEqual(ht.users(), [ u("user\u00e6") ]) + + # test deprecated encoding=None + with self.assertWarningList("``encoding=None`` is deprecated"): + ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding=None) + self.assertEqual(ht.users(), [ b('user\xc3\xa6') ]) + + # check sample latin-1 + ht = apache.HtpasswdFile.from_string(self.sample_04_latin1, + encoding="latin-1", return_unicode=True) + self.assertEqual(ht.users(), [ u("user\u00e6") ]) + + def test_08_get_hash(self): + "test get_hash()" + ht = apache.HtpasswdFile.from_string(self.sample_01) + self.assertEqual(ht.get_hash("user3"), b("{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=")) + self.assertEqual(ht.get_hash("user4"), b("pass4")) + self.assertEqual(ht.get_hash("user5"), None) + + with self.assertWarningList("find\(\) is deprecated"): + self.assertEqual(ht.find("user4"), b("pass4")) - def test_08_to_string(self): + def test_09_to_string(self): "test to_string" - #check with known sample - ht = apache.HtpasswdFile._from_string(self.sample_01) + # check with known sample + ht = apache.HtpasswdFile.from_string(self.sample_01) self.assertEqual(ht.to_string(), self.sample_01) - #test blank + # test blank ht = apache.HtpasswdFile() self.assertEqual(ht.to_string(), b("")) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#htdigest -#========================================================= + def test_10_repr(self): + ht = apache.HtpasswdFile("fakepath", autosave=True, new=True, encoding="latin-1") + repr(ht) + + def test_11_malformed(self): + self.assertRaises(ValueError, apache.HtpasswdFile.from_string, + b('realm:user1:pass1\n')) + self.assertRaises(ValueError, apache.HtpasswdFile.from_string, + b('pass1\n')) + + def test_12_from_string(self): + # forbid path kwd + self.assertRaises(TypeError, apache.HtpasswdFile.from_string, + b(''), path=None) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# htdigest +#============================================================================= class HtdigestFileTest(TestCase): "test HtdigestFile class" - case_prefix = "HtdigestFile" + descriptionPrefix = "HtdigestFile" - sample_01 = b('user2:realm:549d2a5f4659ab39a80dac99e159ab19\nuser3:realm:a500bb8c02f6a9170ae46af10c898744\nuser4:realm:ab7b5d5f28ccc7666315f508c7358519\nuser1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n') - sample_02 = b('user3:realm:a500bb8c02f6a9170ae46af10c898744\nuser4:realm:ab7b5d5f28ccc7666315f508c7358519\n') - sample_03 = b('user2:realm:5ba6d8328943c23c64b50f8b29566059\nuser3:realm:a500bb8c02f6a9170ae46af10c898744\nuser4:realm:ab7b5d5f28ccc7666315f508c7358519\nuser1:realm:2a6cf53e7d8f8cf39d946dc880b14128\nuser5:realm:03c55fdc6bf71552356ad401bdb9af19\n') + # sample with 4 users + sample_01 = b('user2:realm:549d2a5f4659ab39a80dac99e159ab19\n' + 'user3:realm:a500bb8c02f6a9170ae46af10c898744\n' + 'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n' + 'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n') + + # sample 1 with user 1, 2 deleted; 4 changed + sample_02 = b('user3:realm:a500bb8c02f6a9170ae46af10c898744\n' + 'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n') + + # sample 1 with user2 updated, user 1 first entry removed, and user 5 added + sample_03 = b('user2:realm:5ba6d8328943c23c64b50f8b29566059\n' + 'user3:realm:a500bb8c02f6a9170ae46af10c898744\n' + 'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n' + 'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n' + 'user5:realm:03c55fdc6bf71552356ad401bdb9af19\n') + # standalone sample with 8-bit username & realm sample_04_utf8 = b('user\xc3\xa6:realm\xc3\xa6:549d2a5f4659ab39a80dac99e159ab19\n') sample_04_latin1 = b('user\xe6:realm\xe6:549d2a5f4659ab39a80dac99e159ab19\n') def test_00_constructor_autoload(self): "test constructor autoload" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - #check with existing file - path = mktemp() + # check with existing file + path = self.mktemp() set_file(path, self.sample_01) ht = apache.HtdigestFile(path) self.assertEqual(ht.to_string(), self.sample_01) - #check autoload=False - ht = apache.HtdigestFile(path, autoload=False) + # check without autoload + ht = apache.HtdigestFile(path, new=True) self.assertEqual(ht.to_string(), b("")) - #check missing file + # check missing file os.remove(path) self.assertRaises(IOError, apache.HtdigestFile, path) + # NOTE: default_realm option checked via other tests. + def test_01_delete(self): "test delete()" - ht = apache.HtdigestFile._from_string(self.sample_01) + ht = apache.HtdigestFile.from_string(self.sample_01) self.assertTrue(ht.delete("user1", "realm")) self.assertTrue(ht.delete("user2", "realm")) - self.assertTrue(not ht.delete("user5", "realm")) + self.assertFalse(ht.delete("user5", "realm")) + self.assertFalse(ht.delete("user3", "realm5")) self.assertEqual(ht.to_string(), self.sample_02) + # invalid user self.assertRaises(ValueError, ht.delete, "user:", "realm") - def test_02_update(self): + # invalid realm + self.assertRaises(ValueError, ht.delete, "user", "realm:") + + def test_01_delete_autosave(self): + path = self.mktemp() + set_file(path, self.sample_01) + + ht = apache.HtdigestFile(path) + self.assertTrue(ht.delete("user1", "realm")) + self.assertFalse(ht.delete("user3", "realm5")) + self.assertFalse(ht.delete("user5", "realm")) + self.assertEqual(get_file(path), self.sample_01) + + ht.autosave = True + self.assertTrue(ht.delete("user2", "realm")) + self.assertEqual(get_file(path), self.sample_02) + + def test_02_set_password(self): "test update()" - ht = apache.HtdigestFile._from_string(self.sample_01) - self.assertTrue(ht.update("user2", "realm", "pass2x")) - self.assertTrue(not ht.update("user5", "realm", "pass5")) + ht = apache.HtdigestFile.from_string(self.sample_01) + self.assertTrue(ht.set_password("user2", "realm", "pass2x")) + self.assertFalse(ht.set_password("user5", "realm", "pass5")) self.assertEqual(ht.to_string(), self.sample_03) - self.assertRaises(ValueError, ht.update, "user:", "realm", "pass") - self.assertRaises(ValueError, ht.update, "u"*256, "realm", "pass") + # default realm + self.assertRaises(TypeError, ht.set_password, "user2", "pass3") + ht.default_realm = "realm2" + ht.set_password("user2", "pass3") + ht.check_password("user2", "realm2", "pass3") + + # invalid user + self.assertRaises(ValueError, ht.set_password, "user:", "realm", "pass") + self.assertRaises(ValueError, ht.set_password, "u"*256, "realm", "pass") + + # invalid realm + self.assertRaises(ValueError, ht.set_password, "user", "realm:", "pass") + self.assertRaises(ValueError, ht.set_password, "user", "r"*256, "pass") + + # test that legacy update() still works + with self.assertWarningList("update\(\) is deprecated"): + ht.update("user2", "realm2", "test") + self.assertTrue(ht.check_password("user2", "test")) - self.assertRaises(ValueError, ht.update, "user", "realm:", "pass") - self.assertRaises(ValueError, ht.update, "user", "r"*256, "pass") + # TODO: test set_password autosave def test_03_users(self): "test users()" - ht = apache.HtdigestFile._from_string(self.sample_01) - ht.update("user5", "realm", "pass5") + ht = apache.HtdigestFile.from_string(self.sample_01) + ht.set_password("user5", "realm", "pass5") ht.delete("user3", "realm") - ht.update("user3", "realm", "pass3") + ht.set_password("user3", "realm", "pass3") self.assertEqual(ht.users("realm"), ["user2", "user4", "user1", "user5", "user3"]) - def test_04_verify(self): - "test verify()" - ht = apache.HtdigestFile._from_string(self.sample_01) - self.assertTrue(ht.verify("user5", "realm","pass5") is None) - for i in xrange(1,5): + self.assertRaises(TypeError, ht.users, 1) + + def test_04_check_password(self): + "test check_password()" + ht = apache.HtdigestFile.from_string(self.sample_01) + self.assertRaises(TypeError, ht.check_password, 1, 'realm', 'pass5') + self.assertRaises(TypeError, ht.check_password, 'user', 1, 'pass5') + self.assertIs(ht.check_password("user5", "realm","pass5"), None) + for i in irange(1,5): i = str(i) - self.assertTrue(ht.verify("user"+i, "realm", "pass"+i)) - self.assertTrue(ht.verify("user"+i, "realm", "pass5") is False) + self.assertTrue(ht.check_password("user"+i, "realm", "pass"+i)) + self.assertIs(ht.check_password("user"+i, "realm", "pass5"), False) + + # default realm + self.assertRaises(TypeError, ht.check_password, "user5", "pass5") + ht.default_realm = "realm" + self.assertTrue(ht.check_password("user1", "pass1")) + self.assertIs(ht.check_password("user5", "pass5"), None) + + # test that legacy verify() still works + with self.assertWarningList(["verify\(\) is deprecated"]*2): + self.assertTrue(ht.verify("user1", "realm", "pass1")) + self.assertFalse(ht.verify("user1", "realm", "pass2")) - self.assertRaises(ValueError, ht.verify, "user:", "realm", "pass") + # invalid user + self.assertRaises(ValueError, ht.check_password, "user:", "realm", "pass") def test_05_load(self): "test load()" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - #setup empty file - path = mktemp() + # setup empty file + path = self.mktemp() set_file(path, "") backdate_file_mtime(path, 5) ha = apache.HtdigestFile(path) self.assertEqual(ha.to_string(), b("")) - #make changes, check force=False does nothing - ha.update("user1", "realm", "pass1") - ha.load(force=False) + # make changes, check load_if_changed() does nothing + ha.set_password("user1", "realm", "pass1") + ha.load_if_changed() self.assertEqual(ha.to_string(), b('user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n')) - #change file + # change file set_file(path, self.sample_01) - ha.load(force=False) + ha.load_if_changed() self.assertEqual(ha.to_string(), self.sample_01) - #make changes, check force=True overwrites them - ha.update("user5", "realm", "pass5") + # make changes, check load_if_changed overwrites them + ha.set_password("user5", "realm", "pass5") ha.load() self.assertEqual(ha.to_string(), self.sample_01) - #test load w/ no path + # test load w/ no path hb = apache.HtdigestFile() self.assertRaises(RuntimeError, hb.load) - self.assertRaises(RuntimeError, hb.load, force=False) + self.assertRaises(RuntimeError, hb.load_if_changed) + + # test load w/ explicit path + hc = apache.HtdigestFile() + hc.load(path) + self.assertEqual(hc.to_string(), self.sample_01) + + # change file, test deprecated force=False kwd + ensure_mtime_changed(path) + set_file(path, "") + with self.assertWarningList(r"load\(force=False\) is deprecated"): + ha.load(force=False) + self.assertEqual(ha.to_string(), b("")) def test_06_save(self): "test save()" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - #load from file - path = mktemp() + # load from file + path = self.mktemp() set_file(path, self.sample_01) ht = apache.HtdigestFile(path) - #make changes, check they saved + # make changes, check they saved ht.delete("user1", "realm") ht.delete("user2", "realm") ht.save() self.assertEqual(get_file(path), self.sample_02) - #test save w/ no path + # test save w/ no path hb = apache.HtdigestFile() - hb.update("user1", "realm", "pass1") + hb.set_password("user1", "realm", "pass1") self.assertRaises(RuntimeError, hb.save) + # test save w/ explicit path + hb.save(path) + self.assertEqual(get_file(path), hb.to_string()) + def test_07_realms(self): "test realms() & delete_realm()" - ht = apache.HtdigestFile._from_string(self.sample_01) + ht = apache.HtdigestFile.from_string(self.sample_01) self.assertEqual(ht.delete_realm("x"), 0) self.assertEqual(ht.realms(), ['realm']) @@ -338,59 +513,52 @@ self.assertEqual(ht.realms(), []) self.assertEqual(ht.to_string(), b("")) - def test_08_find(self): - "test find()" - ht = apache.HtdigestFile._from_string(self.sample_01) - self.assertEqual(ht.find("user3", "realm"), "a500bb8c02f6a9170ae46af10c898744") - self.assertEqual(ht.find("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519") - self.assertEqual(ht.find("user5", "realm"), None) + def test_08_get_hash(self): + "test get_hash()" + ht = apache.HtdigestFile.from_string(self.sample_01) + self.assertEqual(ht.get_hash("user3", "realm"), "a500bb8c02f6a9170ae46af10c898744") + self.assertEqual(ht.get_hash("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519") + self.assertEqual(ht.get_hash("user5", "realm"), None) + + with self.assertWarningList("find\(\) is deprecated"): + self.assertEqual(ht.find("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519") def test_09_encodings(self): "test encoding parameter" - #test bad encodings cause failure in constructor + # test bad encodings cause failure in constructor self.assertRaises(ValueError, apache.HtdigestFile, encoding="utf-16") - #check users() returns native string by default - ht = apache.HtdigestFile._from_string(self.sample_01) - self.assertIsInstance(ht.realms()[0], native_str) - self.assertIsInstance(ht.users("realm")[0], native_str) - - #check returns unicode if encoding explicitly set - ht = apache.HtdigestFile._from_string(self.sample_01, encoding="utf-8") - self.assertIsInstance(ht.realms()[0], unicode) - self.assertIsInstance(ht.users(u"realm")[0], unicode) - - #check returns bytes if encoding explicitly disabled - ht = apache.HtdigestFile._from_string(self.sample_01, encoding=None) - self.assertIsInstance(ht.realms()[0], bytes) - self.assertIsInstance(ht.users(b("realm"))[0], bytes) - - #check sample utf-8 - ht = apache.HtdigestFile._from_string(self.sample_04_utf8, encoding="utf-8") - self.assertEqual(ht.realms(), [ u"realm\u00e6" ]) - self.assertEqual(ht.users(u"realm\u00e6"), [ u"user\u00e6" ]) - - #check sample latin-1 - ht = apache.HtdigestFile._from_string(self.sample_04_latin1, encoding="latin-1") - self.assertEqual(ht.realms(), [ u"realm\u00e6" ]) - self.assertEqual(ht.users(u"realm\u00e6"), [ u"user\u00e6" ]) - + # check sample utf-8 + ht = apache.HtdigestFile.from_string(self.sample_04_utf8, encoding="utf-8", return_unicode=True) + self.assertEqual(ht.realms(), [ u("realm\u00e6") ]) + self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ]) + + # check sample latin-1 + ht = apache.HtdigestFile.from_string(self.sample_04_latin1, encoding="latin-1", return_unicode=True) + self.assertEqual(ht.realms(), [ u("realm\u00e6") ]) + self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ]) def test_10_to_string(self): "test to_string()" - #check sample - ht = apache.HtdigestFile._from_string(self.sample_01) + # check sample + ht = apache.HtdigestFile.from_string(self.sample_01) self.assertEqual(ht.to_string(), self.sample_01) - #check blank + # check blank ht = apache.HtdigestFile() self.assertEqual(ht.to_string(), b("")) - #========================================================= - #eoc - #========================================================= - -#========================================================= -#EOF -#========================================================= + def test_11_malformed(self): + self.assertRaises(ValueError, apache.HtdigestFile.from_string, + b('realm:user1:pass1:other\n')) + self.assertRaises(ValueError, apache.HtdigestFile.from_string, + b('user1:pass1\n')) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_apps.py passlib-1.6.1/passlib/tests/test_apps.py --- passlib-1.5.3/passlib/tests/test_apps.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_apps.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,31 +1,35 @@ """test passlib.apps""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import logging; log = logging.getLogger(__name__) -#site -#pkg +# site +# pkg from passlib import apps, hash as hashmod from passlib.tests.utils import TestCase -#module +# module -#========================================================= -#test predefined app contexts -#========================================================= +#============================================================================= +# test predefined app contexts +#============================================================================= class AppsTest(TestCase): "perform general tests to make sure contexts work" - #NOTE: these tests are not really comprehensive, - # since they would do little but duplicate - # the presets in apps.py + # NOTE: these tests are not really comprehensive, + # since they would do little but duplicate + # the presets in apps.py # - # they mainly try to ensure no typos - # or dynamic behavior foul-ups. + # they mainly try to ensure no typos + # or dynamic behavior foul-ups. + + def test_master_context(self): + ctx = apps.master_context + self.assertGreater(len(ctx.schemes()), 50) def test_custom_app_context(self): ctx = apps.custom_app_context - self.assertEqual(ctx.policy.schemes(), ["sha512_crypt", "sha256_crypt"]) + self.assertEqual(ctx.schemes(), ("sha512_crypt", "sha256_crypt")) for hash in [ ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'), @@ -90,13 +94,15 @@ ]: self.assertTrue(ctx.verify("test", hash)) - h1 = '$2a$10$Ljj0Kgu7Ddob9xWoqzn0ae.uNfxPRofowWdksk.6jCUHKTGYLD.QG' + h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" if hashmod.bcrypt.has_backend(): self.assertTrue(ctx.verify("test", h1)) - self.assertEqual(ctx.policy.get_handler().name, "bcrypt") + self.assertEqual(ctx.default_scheme(), "bcrypt") + self.assertEqual(ctx.handler().name, "bcrypt") else: self.assertEqual(ctx.identify(h1), "bcrypt") - self.assertEqual(ctx.policy.get_handler().name, "phpass") + self.assertEqual(ctx.default_scheme(), "phpass") + self.assertEqual(ctx.handler().name, "phpass") def test_phpbb3_context(self): ctx = apps.phpbb3_context @@ -117,6 +123,6 @@ ]: self.assertTrue(ctx.verify("test", hash)) -#========================================================= -#eof -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_context.py passlib-1.6.1/passlib/tests/test_context.py --- passlib-1.5.3/passlib/tests/test_context.py 2011-10-08 04:51:13.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_context.py 2012-08-01 20:42:47.000000000 +0000 @@ -1,797 +1,1444 @@ -"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" -#========================================================= -#imports -#========================================================= +"""tests for passlib.context""" +#============================================================================= +# imports +#============================================================================= +# core from __future__ import with_statement -#core +from passlib.utils.compat import PY3 +if PY3: + from configparser import NoSectionError +else: + from ConfigParser import NoSectionError import hashlib -from logging import getLogger +import logging; log = logging.getLogger(__name__) +import re import os import time import warnings import sys -#site -try: - from pkg_resources import resource_filename -except ImportError: - resource_filename = None -#pkg +# site +# pkg from passlib import hash -from passlib.context import CryptContext, CryptPolicy, LazyCryptContext -from passlib.utils import to_bytes, to_unicode +from passlib.context import CryptContext, LazyCryptContext +from passlib.exc import PasslibConfigWarning +from passlib.utils import tick, to_bytes, to_unicode +from passlib.utils.compat import irange, u, unicode, str_to_uascii, PY2 import passlib.utils.handlers as uh -from passlib.tests.utils import TestCase, mktemp, catch_warnings, \ - gae_env, set_file -from passlib.registry import register_crypt_handler_path, has_crypt_handler, \ - _unload_handler_name as unload_handler_name -#module -log = getLogger(__name__) +from passlib.tests.utils import TestCase, catch_warnings, set_file, TICK_RESOLUTION, quicksleep +from passlib.registry import (register_crypt_handler_path, + _has_crypt_handler as has_crypt_handler, + _unload_handler_name as unload_handler_name, + get_crypt_handler, + ) +# local +#============================================================================= +# support +#============================================================================= +here = os.path.abspath(os.path.dirname(__file__)) + +def merge_dicts(first, *args, **kwds): + target = first.copy() + for arg in args: + target.update(arg) + if kwds: + target.update(kwds) + return target -#========================================================= +#============================================================================= # -#========================================================= -class CryptPolicyTest(TestCase): - "test CryptPolicy object" - - #TODO: need to test user categories w/in all this - - case_prefix = "CryptPolicy" - - #========================================================= - #sample crypt policies used for testing - #========================================================= - - #----------------------------------------------------- - #sample 1 - average config file - #----------------------------------------------------- - #NOTE: copy of this is stored in file passlib/tests/sample_config_1s.cfg - sample_config_1s = """\ -[passlib] -schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt -default = md5_crypt -all.vary_rounds = 10%% -bsdi_crypt.max_rounds = 30000 -bsdi_crypt.default_rounds = 25000 -sha512_crypt.max_rounds = 50000 -sha512_crypt.min_rounds = 40000 -""" - sample_config_1s_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), "sample_config_1s.cfg")) - if not os.path.exists(sample_config_1s_path) and resource_filename: - #in case we're zipped up in an egg. - sample_config_1s_path = resource_filename("passlib.tests", - "sample_config_1s.cfg") +#============================================================================= +class CryptContextTest(TestCase): + descriptionPrefix = "CryptContext" + + # TODO: these unittests could really use a good cleanup + # and reorganizing, to ensure they're getting everything. - #make sure sample_config_1s uses \n linesep - tests rely on this - assert sample_config_1s.startswith("[passlib]\nschemes") + #=================================================================== + # sample configurations used in tests + #=================================================================== + + #--------------------------------------------------------------- + # sample 1 - typical configuration + #--------------------------------------------------------------- + sample_1_schemes = ["des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"] + sample_1_handlers = [get_crypt_handler(name) for name in sample_1_schemes] - sample_config_1pd = dict( - schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + sample_1_dict = dict( + schemes = sample_1_schemes, default = "md5_crypt", - all__vary_rounds = "10%", + all__vary_rounds = 0.1, bsdi_crypt__max_rounds = 30000, bsdi_crypt__default_rounds = 25000, sha512_crypt__max_rounds = 50000, sha512_crypt__min_rounds = 40000, ) - sample_config_1pid = { - "schemes": "des_crypt, md5_crypt, bsdi_crypt, sha512_crypt", - "default": "md5_crypt", - "all.vary_rounds": "10%", - "bsdi_crypt.max_rounds": 30000, - "bsdi_crypt.default_rounds": 25000, - "sha512_crypt.max_rounds": 50000, - "sha512_crypt.min_rounds": 40000, - } - - sample_config_1prd = dict( - schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt], - default = hash.md5_crypt, - all__vary_rounds = "10%", - bsdi_crypt__max_rounds = 30000, - bsdi_crypt__default_rounds = 25000, - sha512_crypt__max_rounds = 50000, - sha512_crypt__min_rounds = 40000, - ) + sample_1_resolved_dict = merge_dicts(sample_1_dict, + schemes = sample_1_handlers) - #----------------------------------------------------- - #sample 2 - partial policy & result of overlay on sample 1 - #----------------------------------------------------- - sample_config_2s = """\ + sample_1_unnormalized = u("""\ [passlib] -bsdi_crypt.min_rounds = 29000 -bsdi_crypt.max_rounds = 35000 -bsdi_crypt.default_rounds = 31000 -sha512_crypt.min_rounds = 45000 -""" - - sample_config_2pd = dict( - #using this to test full replacement of existing options - bsdi_crypt__min_rounds = 29000, - bsdi_crypt__max_rounds = 35000, - bsdi_crypt__default_rounds = 31000, - #using this to test partial replacement of existing options - sha512_crypt__min_rounds=45000, - ) - - sample_config_12pd = dict( - schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], - default = "md5_crypt", - all__vary_rounds = "10%", - bsdi_crypt__min_rounds = 29000, - bsdi_crypt__max_rounds = 35000, - bsdi_crypt__default_rounds = 31000, - sha512_crypt__max_rounds = 50000, - sha512_crypt__min_rounds=45000, - ) - - #----------------------------------------------------- - #sample 3 - just changing default - #----------------------------------------------------- - sample_config_3pd = dict( - default="sha512_crypt", - ) +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +; this is using %... +all__vary_rounds = 10%% +; this is using 'rounds' instead of 'default_rounds' +bsdi_crypt__rounds = 25000 +bsdi_crypt__max_rounds = 30000 +sha512_crypt__max_rounds = 50000 +sha512_crypt__min_rounds = 40000 +""") - sample_config_123pd = dict( - schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], - default = "sha512_crypt", - all__vary_rounds = "10%", + sample_1_unicode = u("""\ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all__vary_rounds = 0.1 +bsdi_crypt__default_rounds = 25000 +bsdi_crypt__max_rounds = 30000 +sha512_crypt__max_rounds = 50000 +sha512_crypt__min_rounds = 40000 + +""") + + #--------------------------------------------------------------- + # sample 1 external files + #--------------------------------------------------------------- + + # sample 1 string with '\n' linesep + sample_1_path = os.path.join(here, "sample1.cfg") + + # sample 1 with '\r\n' linesep + sample_1b_unicode = sample_1_unicode.replace(u("\n"), u("\r\n")) + sample_1b_path = os.path.join(here, "sample1b.cfg") + + # sample 1 using UTF-16 and alt section + sample_1c_bytes = sample_1_unicode.replace(u("[passlib]"), + u("[mypolicy]")).encode("utf-16") + sample_1c_path = os.path.join(here, "sample1c.cfg") + + # enable to regenerate sample files + if False: + set_file(sample_1_path, sample_1_unicode) + set_file(sample_1b_path, sample_1b_unicode) + set_file(sample_1c_path, sample_1c_bytes) + + #--------------------------------------------------------------- + # sample 2 & 12 - options patch + #--------------------------------------------------------------- + sample_2_dict = dict( + # using this to test full replacement of existing options bsdi_crypt__min_rounds = 29000, bsdi_crypt__max_rounds = 35000, bsdi_crypt__default_rounds = 31000, - sha512_crypt__max_rounds = 50000, + # using this to test partial replacement of existing options sha512_crypt__min_rounds=45000, ) - #----------------------------------------------------- - #sample 4 - category specific - #----------------------------------------------------- - sample_config_4s = """ + sample_2_unicode = """\ [passlib] -schemes = sha512_crypt -all.vary_rounds = 10%% -default.sha512_crypt.max_rounds = 20000 -admin.all.vary_rounds = 5%% -admin.sha512_crypt.max_rounds = 40000 +bsdi_crypt__min_rounds = 29000 +bsdi_crypt__max_rounds = 35000 +bsdi_crypt__default_rounds = 31000 +sha512_crypt__min_rounds = 45000 """ - sample_config_4pd = dict( - schemes = [ "sha512_crypt" ], - all__vary_rounds = "10%", - sha512_crypt__max_rounds = 20000, - admin__all__vary_rounds = "5%", - admin__sha512_crypt__max_rounds = 40000, - ) + # sample 2 overlayed on top of sample 1 + sample_12_dict = merge_dicts(sample_1_dict, sample_2_dict) - #----------------------------------------------------- - #sample 5 - to_string & deprecation testing - #----------------------------------------------------- - sample_config_5s = sample_config_1s + """\ -deprecated = des_crypt -admin__context__deprecated = des_crypt, bsdi_crypt -""" + #--------------------------------------------------------------- + # sample 3 & 123 - just changing default from sample 1 + #--------------------------------------------------------------- + sample_3_dict = dict( + default="sha512_crypt", + ) + + # sample 3 overlayed on 2 overlayed on 1 + sample_123_dict = merge_dicts(sample_12_dict, sample_3_dict) - sample_config_5pd = sample_config_1pd.copy() - sample_config_5pd.update( - deprecated = [ "des_crypt" ], - admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ], + #--------------------------------------------------------------- + # sample 4 - used by api tests + #--------------------------------------------------------------- + sample_4_dict = dict( + schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt", + "sha256_crypt"], + deprecated = [ "des_crypt", ], + default = "sha256_crypt", + bsdi_crypt__max_rounds = 30, + bsdi_crypt__default_rounds = 25, + bsdi_crypt__vary_rounds = 0, + sha256_crypt__max_rounds = 3000, + sha256_crypt__min_rounds = 2000, + sha256_crypt__default_rounds = 3000, + phpass__ident = "H", + phpass__default_rounds = 7, ) - sample_config_5pid = sample_config_1pid.copy() - sample_config_5pid.update({ - "deprecated": "des_crypt", - "admin.context.deprecated": "des_crypt, bsdi_crypt", - }) - - sample_config_5prd = sample_config_1prd.copy() - sample_config_5prd.update({ - # XXX: should deprecated return the actual handlers in this case? - # would have to modify how policy stores info, for one. - "deprecated": ["des_crypt"], - "admin__context__deprecated": ["des_crypt", "bsdi_crypt"], - }) - - #========================================================= - #constructors - #========================================================= - def test_00_constructor(self): - "test CryptPolicy() constructor" - policy = CryptPolicy(**self.sample_config_1pd) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #check with bad key - self.assertRaises(KeyError, CryptPolicy, - schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], - bad__key__bsdi_crypt__max_rounds = 30000, + #=================================================================== + # constructors + #=================================================================== + def test_01_constructor(self): + "test class constructor" + + # test blank constructor works correctly + ctx = CryptContext() + self.assertEqual(ctx.to_dict(), {}) + + # test sample 1 with scheme=names + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 with scheme=handlers + ctx = CryptContext(**self.sample_1_resolved_dict) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 2: options w/o schemes + ctx = CryptContext(**self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_2_dict) + + # test sample 3: default only + ctx = CryptContext(**self.sample_3_dict) + self.assertEqual(ctx.to_dict(), self.sample_3_dict) + + def test_02_from_string(self): + "test from_string() constructor" + # test sample 1 unicode + ctx = CryptContext.from_string(self.sample_1_unicode) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 with unnormalized inputs + ctx = CryptContext.from_string(self.sample_1_unnormalized) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 utf-8 + ctx = CryptContext.from_string(self.sample_1_unicode.encode("utf-8")) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 w/ '\r\n' linesep + ctx = CryptContext.from_string(self.sample_1b_unicode) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 using UTF-16 and alt section + ctx = CryptContext.from_string(self.sample_1c_bytes, section="mypolicy", + encoding="utf-16") + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test wrong type + self.assertRaises(TypeError, CryptContext.from_string, None) + + # test missing section + self.assertRaises(NoSectionError, CryptContext.from_string, + self.sample_1_unicode, section="fakesection") + + def test_03_from_path(self): + "test from_path() constructor" + # make sure sample files exist + if not os.path.exists(self.sample_1_path): + raise RuntimeError("can't find data file: %r" % self.sample_1_path) + + # test sample 1 + ctx = CryptContext.from_path(self.sample_1_path) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 w/ '\r\n' linesep + ctx = CryptContext.from_path(self.sample_1b_path) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test sample 1 encoding using UTF-16 and alt section + ctx = CryptContext.from_path(self.sample_1c_path, section="mypolicy", + encoding="utf-16") + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test missing file + self.assertRaises(EnvironmentError, CryptContext.from_path, + os.path.join(here, "sample1xxx.cfg")) + + # test missing section + self.assertRaises(NoSectionError, CryptContext.from_path, + self.sample_1_path, section="fakesection") + + def test_04_copy(self): + "test copy() method" + cc1 = CryptContext(**self.sample_1_dict) + + # overlay sample 2 onto copy + cc2 = cc1.copy(**self.sample_2_dict) + self.assertEqual(cc1.to_dict(), self.sample_1_dict) + self.assertEqual(cc2.to_dict(), self.sample_12_dict) + + # check that repeating overlay makes no change + cc2b = cc2.copy(**self.sample_2_dict) + self.assertEqual(cc1.to_dict(), self.sample_1_dict) + self.assertEqual(cc2b.to_dict(), self.sample_12_dict) + + # overlay sample 3 on copy + cc3 = cc2.copy(**self.sample_3_dict) + self.assertEqual(cc3.to_dict(), self.sample_123_dict) + + # test empty copy creates separate copy + cc4 = cc1.copy() + self.assertIsNot(cc4, cc1) + self.assertEqual(cc1.to_dict(), self.sample_1_dict) + self.assertEqual(cc4.to_dict(), self.sample_1_dict) + + # ... and that modifying copy doesn't affect original + cc4.update(**self.sample_2_dict) + self.assertEqual(cc1.to_dict(), self.sample_1_dict) + self.assertEqual(cc4.to_dict(), self.sample_12_dict) + + def test_09_repr(self): + "test repr()" + cc1 = CryptContext(**self.sample_1_dict) + self.assertRegex(repr(cc1), "^$") + + #=================================================================== + # modifiers + #=================================================================== + def test_10_load(self): + "test load() / load_path() method" + # NOTE: load() is the workhorse that handles all policy parsing, + # compilation, and validation. most of it's features are tested + # elsewhere, since all the constructors and modifiers are just + # wrappers for it. + + # source_type 'auto' + ctx = CryptContext() + + # detect dict + ctx.load(self.sample_1_dict) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # detect unicode string + ctx.load(self.sample_1_unicode) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # detect bytes string + ctx.load(self.sample_1_unicode.encode("utf-8")) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # anything else - TypeError + self.assertRaises(TypeError, ctx.load, None) + + # NOTE: load_path() tested by from_path() + # NOTE: additional string tests done by from_string() + + # update flag - tested by update() method tests + # encoding keyword - tested by from_string() & from_path() + # section keyword - tested by from_string() & from_path() + + # test load empty + ctx = CryptContext(**self.sample_1_dict) + ctx.load({}, update=True) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # multiple loads should clear the state + ctx = CryptContext() + ctx.load(self.sample_1_dict) + ctx.load(self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_2_dict) + + def test_11_load_rollback(self): + "test load() errors restore old state" + # create initial context + cc = CryptContext(["des_crypt", "sha256_crypt"], + sha256_crypt__default_rounds=5000, + all__vary_rounds=0.1, ) + result = cc.to_string() - #check with bad handler - self.assertRaises(TypeError, CryptPolicy, schemes=[uh.StaticHandler]) + # do an update operation that should fail during parsing + # XXX: not sure what the right error type is here. + self.assertRaises(TypeError, cc.update, too__many__key__parts=True) + self.assertEqual(cc.to_string(), result) + + # do an update operation that should fail during extraction + # FIXME: this isn't failing even in broken case, need to figure out + # way to ensure some keys come after this one. + self.assertRaises(KeyError, cc.update, fake_context_option=True) + self.assertEqual(cc.to_string(), result) + + # do an update operation that should fail during compilation + self.assertRaises(ValueError, cc.update, sha256_crypt__min_rounds=10000) + self.assertEqual(cc.to_string(), result) + + def test_12_update(self): + "test update() method" + + # empty overlay + ctx = CryptContext(**self.sample_1_dict) + ctx.update() + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + + # test basic overlay + ctx = CryptContext(**self.sample_1_dict) + ctx.update(**self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_12_dict) + + # ... and again + ctx.update(**self.sample_3_dict) + self.assertEqual(ctx.to_dict(), self.sample_123_dict) + + # overlay w/ dict arg + ctx = CryptContext(**self.sample_1_dict) + ctx.update(self.sample_2_dict) + self.assertEqual(ctx.to_dict(), self.sample_12_dict) + + # overlay w/ string + ctx = CryptContext(**self.sample_1_dict) + ctx.update(self.sample_2_unicode) + self.assertEqual(ctx.to_dict(), self.sample_12_dict) + + # too many args + self.assertRaises(TypeError, ctx.update, {}, {}) + self.assertRaises(TypeError, ctx.update, {}, schemes=['des_crypt']) + + # wrong arg type + self.assertRaises(TypeError, ctx.update, None) + + #=================================================================== + # option parsing + #=================================================================== + def test_20_options(self): + "test basic option parsing" + def parse(**kwds): + return CryptContext(**kwds).to_dict() + + # + # common option parsing tests + # + + # test keys with blank fields are rejected + # blank option + self.assertRaises(TypeError, CryptContext, __=0.1) + self.assertRaises(TypeError, CryptContext, default__scheme__='x') + + # blank scheme + self.assertRaises(TypeError, CryptContext, __option='x') + self.assertRaises(TypeError, CryptContext, default____option='x') + + # blank category + self.assertRaises(TypeError, CryptContext, __scheme__option='x') + + # test keys with too many field are rejected + self.assertRaises(TypeError, CryptContext, + category__scheme__option__invalid = 30000) + + # keys with mixed separators should be handled correctly. + # (testing actual data, not to_dict(), since re-render hid original bug) + self.assertRaises(KeyError, parse, + **{"admin.context__schemes":"md5_crypt"}) + ctx = CryptContext(**{"schemes":"md5_crypt,des_crypt", + "admin.context__default":"des_crypt"}) + self.assertEqual(ctx.default_scheme("admin"), "des_crypt") + + # + # context option -specific tests + # + + # test context option key parsing + result = dict(default="md5_crypt") + self.assertEqual(parse(default="md5_crypt"), result) + self.assertEqual(parse(context__default="md5_crypt"), result) + self.assertEqual(parse(default__context__default="md5_crypt"), result) + self.assertEqual(parse(**{"context.default":"md5_crypt"}), result) + self.assertEqual(parse(**{"default.context.default":"md5_crypt"}), result) + + # test context option key parsing w/ category + result = dict(admin__context__default="md5_crypt") + self.assertEqual(parse(admin__context__default="md5_crypt"), result) + self.assertEqual(parse(**{"admin.context.default":"md5_crypt"}), result) + + # + # hash option -specific tests + # + + # test hash option key parsing + result = dict(all__vary_rounds=0.1) + self.assertEqual(parse(all__vary_rounds=0.1), result) + self.assertEqual(parse(default__all__vary_rounds=0.1), result) + self.assertEqual(parse(**{"all.vary_rounds":0.1}), result) + self.assertEqual(parse(**{"default.all.vary_rounds":0.1}), result) + + # test hash option key parsing w/ category + result = dict(admin__all__vary_rounds=0.1) + self.assertEqual(parse(admin__all__vary_rounds=0.1), result) + self.assertEqual(parse(**{"admin.all.vary_rounds":0.1}), result) + + # settings not allowed if not in hash.settings_kwds + ctx = CryptContext(["phpass", "md5_crypt"], phpass__ident="P") + self.assertRaises(KeyError, ctx.copy, md5_crypt__ident="P") + + # hash options 'salt' and 'rounds' not allowed + self.assertRaises(KeyError, CryptContext, schemes=["des_crypt"], + des_crypt__salt="xx") + self.assertRaises(KeyError, CryptContext, schemes=["des_crypt"], + all__salt="xx") + + def test_21_schemes(self): + "test 'schemes' context option parsing" + + # schemes can be empty + cc = CryptContext(schemes=None) + self.assertEqual(cc.schemes(), ()) + + # schemes can be list of names + cc = CryptContext(schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt")) + + # schemes can be comma-sep string + cc = CryptContext(schemes=" des_crypt, md5_crypt, ") + self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt")) + + # schemes can be list of handlers + cc = CryptContext(schemes=[hash.des_crypt, hash.md5_crypt]) + self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt")) + + # scheme must be name or handler + self.assertRaises(TypeError, CryptContext, schemes=[uh.StaticHandler]) + + # handlers must have a name + class nameless(uh.StaticHandler): + name = None + self.assertRaises(ValueError, CryptContext, schemes=[nameless]) - #check with multiple handlers + # names must be unique class dummy_1(uh.StaticHandler): name = 'dummy_1' - self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1]) + self.assertRaises(KeyError, CryptContext, schemes=[dummy_1, dummy_1]) - #with unknown deprecated value - self.assertRaises(KeyError, CryptPolicy, - schemes=['des_crypt'], - deprecated=['md5_crypt']) + # schemes not allowed per-category + self.assertRaises(KeyError, CryptContext, + admin__context__schemes=["md5_crypt"]) + + def test_22_deprecated(self): + "test 'deprecated' context option parsing" + def getdep(ctx, category=None): + return [name for name in ctx.schemes() + if ctx._is_deprecated_scheme(name, category)] + + # no schemes - all deprecated values allowed + cc = CryptContext(deprecated=["md5_crypt"]) + cc.update(schemes=["md5_crypt", "des_crypt"]) + self.assertEqual(getdep(cc),["md5_crypt"]) + + # deprecated values allowed if subset of schemes + cc = CryptContext(deprecated=["md5_crypt"], schemes=["md5_crypt", "des_crypt"]) + self.assertEqual(getdep(cc), ["md5_crypt"]) + + # can be handler + # XXX: allow handlers in deprecated list? not for now. + self.assertRaises(TypeError, CryptContext, deprecated=[hash.md5_crypt], + schemes=["md5_crypt", "des_crypt"]) +## cc = CryptContext(deprecated=[hash.md5_crypt], schemes=["md5_crypt", "des_crypt"]) +## self.assertEqual(getdep(cc), ["md5_crypt"]) + + # comma sep list + cc = CryptContext(deprecated="md5_crypt,des_crypt", schemes=["md5_crypt", "des_crypt", "sha256_crypt"]) + self.assertEqual(getdep(cc), ["md5_crypt", "des_crypt"]) + + # values outside of schemes not allowed + self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'], + deprecated=['md5_crypt']) - #with unknown default value - self.assertRaises(KeyError, CryptPolicy, + # deprecating ALL schemes should cause ValueError + self.assertRaises(ValueError, CryptContext, schemes=['des_crypt'], - default='md5_crypt') - - def test_01_from_path_simple(self): - "test CryptPolicy.from_path() constructor" - #NOTE: this is separate so it can also run under GAE - - #test preset stored in existing file - path = self.sample_config_1s_path - policy = CryptPolicy.from_path(path) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test if path missing - self.assertRaises(EnvironmentError, CryptPolicy.from_path, path + 'xxx') - - def test_01_from_path(self): - "test CryptPolicy.from_path() constructor with encodings" - if gae_env: - return self.skipTest("GAE doesn't offer read/write filesystem access") - - path = mktemp() - - #test "\n" linesep - set_file(path, self.sample_config_1s) - policy = CryptPolicy.from_path(path) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test "\r\n" linesep - set_file(path, self.sample_config_1s.replace("\n","\r\n")) - policy = CryptPolicy.from_path(path) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test with custom encoding - uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8") - set_file(path, uc2) - policy = CryptPolicy.from_path(path, encoding="utf-16") - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - def test_02_from_string(self): - "test CryptPolicy.from_string() constructor" - #test "\n" linesep - policy = CryptPolicy.from_string(self.sample_config_1s) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test "\r\n" linesep - policy = CryptPolicy.from_string( - self.sample_config_1s.replace("\n","\r\n")) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test with unicode - data = to_unicode(self.sample_config_1s) - policy = CryptPolicy.from_string(data) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test with non-ascii-compatible encoding - uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8") - policy = CryptPolicy.from_string(uc2, encoding="utf-16") - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #test category specific options - policy = CryptPolicy.from_string(self.sample_config_4s) - self.assertEqual(policy.to_dict(), self.sample_config_4pd) - - def test_03_from_source(self): - "test CryptPolicy.from_source() constructor" - #pass it a path - policy = CryptPolicy.from_source(self.sample_config_1s_path) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #pass it a string - policy = CryptPolicy.from_source(self.sample_config_1s) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #pass it a dict (NOTE: make a copy to detect in-place modifications) - policy = CryptPolicy.from_source(self.sample_config_1pd.copy()) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #pass it existing policy - p2 = CryptPolicy.from_source(policy) - self.assertIs(policy, p2) - - #pass it something wrong - self.assertRaises(TypeError, CryptPolicy.from_source, 1) - self.assertRaises(TypeError, CryptPolicy.from_source, []) - - def test_04_from_sources(self): - "test CryptPolicy.from_sources() constructor" - - #pass it empty list - self.assertRaises(ValueError, CryptPolicy.from_sources, []) - - #pass it one-element list - policy = CryptPolicy.from_sources([self.sample_config_1s]) - self.assertEqual(policy.to_dict(), self.sample_config_1pd) - - #pass multiple sources - policy = CryptPolicy.from_sources( - [ - self.sample_config_1s_path, - self.sample_config_2s, - self.sample_config_3pd, - ]) - self.assertEqual(policy.to_dict(), self.sample_config_123pd) - - def test_05_replace(self): - "test CryptPolicy.replace() constructor" - - p1 = CryptPolicy(**self.sample_config_1pd) - - #check overlaying sample 2 - p2 = p1.replace(**self.sample_config_2pd) - self.assertEqual(p2.to_dict(), self.sample_config_12pd) - - #check repeating overlay makes no change - p2b = p2.replace(**self.sample_config_2pd) - self.assertEqual(p2b.to_dict(), self.sample_config_12pd) - - #check overlaying sample 3 - p3 = p2.replace(self.sample_config_3pd) - self.assertEqual(p3.to_dict(), self.sample_config_123pd) - - def test_06_forbidden(self): - "test CryptPolicy() forbidden kwds" - - #salt not allowed to be set - self.assertRaises(KeyError, CryptPolicy, - schemes=["des_crypt"], - des_crypt__salt="xx", - ) - self.assertRaises(KeyError, CryptPolicy, - schemes=["des_crypt"], - all__salt="xx", - ) - - #schemes not allowed for category - self.assertRaises(KeyError, CryptPolicy, - schemes=["des_crypt"], - user__context__schemes=["md5_crypt"], + deprecated=['des_crypt']) + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__deprecated=['des_crypt', 'md5_crypt']) + + # deprecating explicit default scheme should cause ValueError + + # ... default listed as deprecated + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + default="md5_crypt", + deprecated="md5_crypt") + + # ... global default deprecated per-category + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + default="md5_crypt", + admin__context__deprecated="md5_crypt") + + # ... category default deprecated globally + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__default="md5_crypt", + deprecated="md5_crypt") + + # ... category default deprecated in category + self.assertRaises(ValueError, CryptContext, + schemes=['des_crypt', 'md5_crypt'], + admin__context__default="md5_crypt", + admin__context__deprecated="md5_crypt") + + # category deplist should shadow default deplist + CryptContext( + schemes=['des_crypt', 'md5_crypt'], + deprecated="md5_crypt", + admin__context__default="md5_crypt", + admin__context__deprecated=[]) + + # wrong type + self.assertRaises(TypeError, CryptContext, deprecated=123) + + # deprecated per-category + cc = CryptContext(deprecated=["md5_crypt"], + schemes=["md5_crypt", "des_crypt"], + admin__context__deprecated=["des_crypt"], + ) + self.assertEqual(getdep(cc), ["md5_crypt"]) + self.assertEqual(getdep(cc, "user"), ["md5_crypt"]) + self.assertEqual(getdep(cc, "admin"), ["des_crypt"]) + + # blank per-category deprecated list, shadowing default list + cc = CryptContext(deprecated=["md5_crypt"], + schemes=["md5_crypt", "des_crypt"], + admin__context__deprecated=[], + ) + self.assertEqual(getdep(cc), ["md5_crypt"]) + self.assertEqual(getdep(cc, "user"), ["md5_crypt"]) + self.assertEqual(getdep(cc, "admin"), []) + + def test_23_default(self): + "test 'default' context option parsing" + + # anything allowed if no schemes + self.assertEqual(CryptContext(default="md5_crypt").to_dict(), + dict(default="md5_crypt")) + + # default allowed if in scheme list + ctx = CryptContext(default="md5_crypt", schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "md5_crypt") + + # default can be handler + # XXX: sure we want to allow this ? maybe deprecate in future. + ctx = CryptContext(default=hash.md5_crypt, schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "md5_crypt") + + # implicit default should be first non-deprecated scheme + ctx = CryptContext(schemes=["des_crypt", "md5_crypt"]) + self.assertEqual(ctx.default_scheme(), "des_crypt") + ctx.update(deprecated="des_crypt") + self.assertEqual(ctx.default_scheme(), "md5_crypt") + + # error if not in scheme list + self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'], + default='md5_crypt') + + # wrong type + self.assertRaises(TypeError, CryptContext, default=1) + + # per-category + ctx = CryptContext(default="des_crypt", + schemes=["des_crypt", "md5_crypt"], + admin__context__default="md5_crypt") + self.assertEqual(ctx.default_scheme(), "des_crypt") + self.assertEqual(ctx.default_scheme("user"), "des_crypt") + self.assertEqual(ctx.default_scheme("admin"), "md5_crypt") + + def test_24_vary_rounds(self): + "test 'vary_rounds' hash option parsing" + def parse(v): + return CryptContext(all__vary_rounds=v).to_dict()['all__vary_rounds'] + + # floats should be preserved + self.assertEqual(parse(0.1), 0.1) + self.assertEqual(parse('0.1'), 0.1) + + # 'xx%' should be converted to float + self.assertEqual(parse('10%'), 0.1) + + # ints should be preserved + self.assertEqual(parse(1000), 1000) + self.assertEqual(parse('1000'), 1000) + + #=================================================================== + # inspection & serialization + #=================================================================== + def test_30_schemes(self): + "test schemes() method" + # NOTE: also checked under test_21 + + # test empty + ctx = CryptContext() + self.assertEqual(ctx.schemes(), ()) + self.assertEqual(ctx.schemes(resolve=True), ()) + + # test sample 1 + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.schemes(), tuple(self.sample_1_schemes)) + self.assertEqual(ctx.schemes(resolve=True), tuple(self.sample_1_handlers)) + + # test sample 2 + ctx = CryptContext(**self.sample_2_dict) + self.assertEqual(ctx.schemes(), ()) + + def test_31_default_scheme(self): + "test default_scheme() method" + # NOTE: also checked under test_23 + + # test empty + ctx = CryptContext() + self.assertRaises(KeyError, ctx.default_scheme) + + # test sample 1 + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.default_scheme(), "md5_crypt") + self.assertEqual(ctx.default_scheme(resolve=True), hash.md5_crypt) + + # test sample 2 + ctx = CryptContext(**self.sample_2_dict) + self.assertRaises(KeyError, ctx.default_scheme) + + # test defaults to first in scheme + ctx = CryptContext(schemes=self.sample_1_schemes) + self.assertEqual(ctx.default_scheme(), "des_crypt") + + # categories tested under test_23 + + def test_32_handler(self): + "test handler() method" + + # default for empty + ctx = CryptContext() + self.assertRaises(KeyError, ctx.handler) + self.assertRaises(KeyError, ctx.handler, "md5_crypt") + + # default for sample 1 + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.handler(), hash.md5_crypt) + + # by name + self.assertEqual(ctx.handler("des_crypt"), hash.des_crypt) + + # name not in schemes + self.assertRaises(KeyError, ctx.handler, "mysql323") + + # check handler() honors category default + ctx = CryptContext("sha256_crypt,md5_crypt", admin__context__default="md5_crypt") + self.assertEqual(ctx.handler(), hash.sha256_crypt) + self.assertEqual(ctx.handler(category="staff"), hash.sha256_crypt) + self.assertEqual(ctx.handler(category="admin"), hash.md5_crypt) + + # test unicode category strings are accepted under py2 + if PY2: + self.assertEqual(ctx.handler(category=u("staff")), hash.sha256_crypt) + self.assertEqual(ctx.handler(category=u("admin")), hash.md5_crypt) + + def test_33_options(self): + "test internal _get_record_options() method" + def options(ctx, scheme, category=None): + return ctx._config._get_record_options_with_flag(scheme, category)[0] + + # this checks that (3 schemes, 3 categories) inherit options correctly. + # the 'user' category is not present in the options. + cc4 = CryptContext( + schemes = [ "sha512_crypt", "des_crypt", "bsdi_crypt"], + deprecated = ["sha512_crypt", "des_crypt"], + all__vary_rounds = 0.1, + bsdi_crypt__vary_rounds=0.2, + sha512_crypt__max_rounds = 20000, + admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ], + admin__all__vary_rounds = 0.05, + admin__bsdi_crypt__vary_rounds=0.3, + admin__sha512_crypt__max_rounds = 40000, ) + self.assertEqual(cc4._config.categories, ("admin",)) - #========================================================= - #reading - #========================================================= - def test_10_has_schemes(self): - "test has_schemes() method" - - p1 = CryptPolicy(**self.sample_config_1pd) - self.assertTrue(p1.has_schemes()) - - p3 = CryptPolicy(**self.sample_config_3pd) - self.assertTrue(not p3.has_schemes()) - - def test_11_iter_handlers(self): - "test iter_handlers() method" - - p1 = CryptPolicy(**self.sample_config_1pd) - s = self.sample_config_1prd['schemes'] - self.assertEqual(list(p1.iter_handlers()), s) - - p3 = CryptPolicy(**self.sample_config_3pd) - self.assertEqual(list(p3.iter_handlers()), []) - - def test_12_get_handler(self): - "test get_handler() method" - - p1 = CryptPolicy(**self.sample_config_1pd) - - #check by name - self.assertIs(p1.get_handler("bsdi_crypt"), hash.bsdi_crypt) - - #check by missing name - self.assertIs(p1.get_handler("sha256_crypt"), None) - self.assertRaises(KeyError, p1.get_handler, "sha256_crypt", required=True) - - #check default - self.assertIs(p1.get_handler(), hash.md5_crypt) + # + # sha512_crypt + # + self.assertEqual(options(cc4, "sha512_crypt"), dict( + deprecated=True, + vary_rounds=0.1, # inherited from all__ + max_rounds=20000, + )) - def test_13_get_options(self): - "test get_options() method" + self.assertEqual(options(cc4, "sha512_crypt", "user"), dict( + deprecated=True, # unconfigured category inherits from default + vary_rounds=0.1, + max_rounds=20000, + )) - p12 = CryptPolicy(**self.sample_config_12pd) + self.assertEqual(options(cc4, "sha512_crypt", "admin"), dict( + # NOT deprecated - context option overridden per-category + vary_rounds=0.05, # global overridden per-cateogry + max_rounds=40000, # overridden per-category + )) - self.assertEqual(p12.get_options("bsdi_crypt"),dict( - vary_rounds = "10%", - min_rounds = 29000, - max_rounds = 35000, - default_rounds = 31000, + # + # des_crypt + # + self.assertEqual(options(cc4, "des_crypt"), dict( + deprecated=True, + vary_rounds=0.1, )) - self.assertEqual(p12.get_options("sha512_crypt"),dict( - vary_rounds = "10%", - min_rounds = 45000, - max_rounds = 50000, + self.assertEqual(options(cc4, "des_crypt", "user"), dict( + deprecated=True, # unconfigured category inherits from default + vary_rounds=0.1, )) - p4 = CryptPolicy.from_string(self.sample_config_4s) - self.assertEqual(p4.get_options("sha512_crypt"), dict( - vary_rounds="10%", - max_rounds=20000, + self.assertEqual(options(cc4, "des_crypt", "admin"), dict( + deprecated=True, # unchanged though overidden + vary_rounds=0.05, # global overridden per-cateogry )) - self.assertEqual(p4.get_options("sha512_crypt", "user"), dict( - vary_rounds="10%", - max_rounds=20000, + # + # bsdi_crypt + # + self.assertEqual(options(cc4, "bsdi_crypt"), dict( + vary_rounds=0.2, # overridden from all__vary_rounds )) - self.assertEqual(p4.get_options("sha512_crypt", "admin"), dict( - vary_rounds="5%", - max_rounds=40000, + self.assertEqual(options(cc4, "bsdi_crypt", "user"), dict( + vary_rounds=0.2, # unconfigured category inherits from default )) - def test_14_handler_is_deprecated(self): - "test handler_is_deprecated() method" - pa = CryptPolicy(**self.sample_config_1pd) - pb = CryptPolicy(**self.sample_config_5pd) - - self.assertFalse(pa.handler_is_deprecated("des_crypt")) - self.assertFalse(pa.handler_is_deprecated(hash.bsdi_crypt)) - self.assertFalse(pa.handler_is_deprecated("sha512_crypt")) - - self.assertTrue(pb.handler_is_deprecated("des_crypt")) - self.assertFalse(pb.handler_is_deprecated(hash.bsdi_crypt)) - self.assertFalse(pb.handler_is_deprecated("sha512_crypt")) - - #check categories as well - self.assertTrue(pb.handler_is_deprecated("des_crypt", "user")) - self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "user")) - self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin")) - self.assertTrue(pb.handler_is_deprecated("bsdi_crypt", "admin")) - - def test_15_min_verify_time(self): - pa = CryptPolicy() - self.assertEqual(pa.get_min_verify_time(), 0) - self.assertEqual(pa.get_min_verify_time('admin'), 0) - - pb = pa.replace(min_verify_time=.1) - self.assertEqual(pb.get_min_verify_time(), .1) - self.assertEqual(pb.get_min_verify_time('admin'), .1) - - pc = pa.replace(admin__context__min_verify_time=.2) - self.assertEqual(pc.get_min_verify_time(), 0) - self.assertEqual(pc.get_min_verify_time('admin'), .2) - - pd = pb.replace(admin__context__min_verify_time=.2) - self.assertEqual(pd.get_min_verify_time(), .1) - self.assertEqual(pd.get_min_verify_time('admin'), .2) - - #TODO: test this. - ##def test_gen_min_verify_time(self): - ## "test get_min_verify_time() method" - - #========================================================= - #serialization - #========================================================= - def test_20_iter_config(self): - "test iter_config() method" - p5 = CryptPolicy(**self.sample_config_5pd) - self.assertEqual(dict(p5.iter_config()), self.sample_config_5pd) - self.assertEqual(dict(p5.iter_config(resolve=True)), self.sample_config_5prd) - self.assertEqual(dict(p5.iter_config(ini=True)), self.sample_config_5pid) + self.assertEqual(options(cc4, "bsdi_crypt", "admin"), dict( + vary_rounds=0.3, + deprecated=True, # deprecation set per-category + )) - def test_21_to_dict(self): + def test_34_to_dict(self): "test to_dict() method" - p5 = CryptPolicy(**self.sample_config_5pd) - self.assertEqual(p5.to_dict(), self.sample_config_5pd) - self.assertEqual(p5.to_dict(resolve=True), self.sample_config_5prd) + # NOTE: this is tested all throughout this test case. + ctx = CryptContext(**self.sample_1_dict) + self.assertEqual(ctx.to_dict(), self.sample_1_dict) + self.assertEqual(ctx.to_dict(resolve=True), self.sample_1_resolved_dict) - def test_22_to_string(self): + def test_35_to_string(self): "test to_string() method" - pa = CryptPolicy(**self.sample_config_5pd) - s = pa.to_string() #NOTE: can't compare string directly, ordering etc may not match - pb = CryptPolicy.from_string(s) - self.assertEqual(pb.to_dict(), self.sample_config_5pd) - - #========================================================= - # - #========================================================= - -#========================================================= -#CryptContext -#========================================================= -class CryptContextTest(TestCase): - "test CryptContext object's behavior" - case_prefix = "CryptContext" - #========================================================= - #constructor - #========================================================= - def test_00_constructor(self): - "test constructor" - #create crypt context using handlers - cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt]) - c,b,a = cc.policy.iter_handlers() - self.assertIs(a, hash.des_crypt) - self.assertIs(b, hash.bsdi_crypt) - self.assertIs(c, hash.md5_crypt) - - #create context using names - cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) - c,b,a = cc.policy.iter_handlers() - self.assertIs(a, hash.des_crypt) - self.assertIs(b, hash.bsdi_crypt) - self.assertIs(c, hash.md5_crypt) - - #TODO: test policy & other options - - def test_01_replace(self): - "test replace()" - - cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) - self.assertIs(cc.policy.get_handler(), hash.md5_crypt) - - cc2 = cc.replace() - self.assertIsNot(cc2, cc) - self.assertIs(cc2.policy, cc.policy) - - cc3 = cc.replace(default="bsdi_crypt") - self.assertIsNot(cc3, cc) - self.assertIsNot(cc3.policy, cc.policy) - self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt) - - def test_02_no_handlers(self): - "test no handlers" - - #check constructor... - cc = CryptContext() - self.assertRaises(KeyError, cc.identify, 'hash', required=True) - self.assertRaises(KeyError, cc.encrypt, 'secret') - self.assertRaises(KeyError, cc.verify, 'secret', 'hash') - - #check updating policy after the fact... - cc = CryptContext(['md5_crypt']) - p = CryptPolicy(schemes=[]) - cc.policy = p - - self.assertRaises(KeyError, cc.identify, 'hash', required=True) - self.assertRaises(KeyError, cc.encrypt, 'secret') - self.assertRaises(KeyError, cc.verify, 'secret', 'hash') + # create ctx and serialize + ctx = CryptContext(**self.sample_1_dict) + dump = ctx.to_string() + + # check ctx->string returns canonical format. + # NOTE: ConfigParser for PY26 and earlier didn't use OrderedDict, + # so to_string() won't get order correct. + # so we skip this test. + import sys + if sys.version_info >= (2,7): + self.assertEqual(dump, self.sample_1_unicode) + + # check ctx->string->ctx->dict returns original + ctx2 = CryptContext.from_string(dump) + self.assertEqual(ctx2.to_dict(), self.sample_1_dict) + + # test section kwd is honored + other = ctx.to_string(section="password-security") + self.assertEqual(other, dump.replace("[passlib]","[password-security]")) + + # test unmanaged handler warning + from passlib import hash + from passlib.tests.test_utils_handlers import UnsaltedHash + ctx3 = CryptContext([UnsaltedHash, "md5_crypt"]) + dump = ctx3.to_string() + self.assertRegex(dump, r"# NOTE: the 'unsalted_test_hash' handler\(s\)" + r" are not registered with Passlib") + + #=================================================================== + # password hash api + #=================================================================== + nonstring_vectors = [ + (None, {}), + (None, {"scheme": "des_crypt"}), + (1, {}), + ((), {}), + ] - #========================================================= - #policy adaptation - #========================================================= - sample_policy_1 = dict( - schemes = [ "des_crypt", "md5_crypt", "nthash", "bsdi_crypt", "sha256_crypt"], - deprecated = [ "des_crypt", ], - default = "sha256_crypt", - bsdi_crypt__max_rounds = 30, - bsdi_crypt__default_rounds = 25, - bsdi_crypt__vary_rounds = 0, - sha256_crypt__max_rounds = 3000, - sha256_crypt__min_rounds = 2000, - sha256_crypt__default_rounds = 3000, - nthash__ident = "NT", - ) + def test_40_basic(self): + "test basic encrypt/identify/verify functionality" + handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt] + cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) - def test_10_genconfig_settings(self): - "test genconfig() honors policy settings" - cc = CryptContext(policy=None, **self.sample_policy_1) + # run through handlers + for crypt in handlers: + h = cc.encrypt("test", scheme=crypt.name) + self.assertEqual(cc.identify(h), crypt.name) + self.assertEqual(cc.identify(h, resolve=True), crypt) + self.assertTrue(cc.verify('test', h)) + self.assertFalse(cc.verify('notest', h)) - # hash specific settings - self.assertEqual( - cc.genconfig(scheme="nthash"), - '$NT$00000000000000000000000000000000', - ) - self.assertEqual( - cc.genconfig(scheme="nthash", ident="3"), - '$3$$00000000000000000000000000000000', - ) + # test default + h = cc.encrypt("test") + self.assertEqual(cc.identify(h), "md5_crypt") - # min rounds - self.assertEqual( - cc.genconfig(rounds=1999, salt="nacl"), - '$5$rounds=2000$nacl$', - ) - self.assertEqual( - cc.genconfig(rounds=2001, salt="nacl"), - '$5$rounds=2001$nacl$' - ) + # test genhash + h = cc.genhash('secret', cc.genconfig()) + self.assertEqual(cc.identify(h), 'md5_crypt') - #max rounds - self.assertEqual( - cc.genconfig(rounds=2999, salt="nacl"), - '$5$rounds=2999$nacl$', - ) - self.assertEqual( - cc.genconfig(rounds=3001, salt="nacl"), - '$5$rounds=3000$nacl$' - ) + h = cc.genhash('secret', cc.genconfig(), scheme='md5_crypt') + self.assertEqual(cc.identify(h), 'md5_crypt') - #default rounds - specified - self.assertEqual( - cc.genconfig(scheme="bsdi_crypt", salt="nacl"), - '_N...nacl', - ) + self.assertRaises(ValueError, cc.genhash, 'secret', cc.genconfig(), scheme="des_crypt") - #default rounds - fall back to max rounds - self.assertEqual( - cc.genconfig(salt="nacl"), - '$5$rounds=3000$nacl$', - ) + def test_41_genconfig(self): + "test genconfig() method" + cc = CryptContext(schemes=["md5_crypt", "phpass"], + phpass__ident="H", + phpass__default_rounds=7, + admin__phpass__ident="P", + ) + + # uses default scheme + self.assertTrue(cc.genconfig().startswith("$1$")) + + # override scheme + self.assertTrue(cc.genconfig(scheme="phpass").startswith("$H$5")) + + # category override + self.assertTrue(cc.genconfig(scheme="phpass", category="admin").startswith("$P$5")) + self.assertTrue(cc.genconfig(scheme="phpass", category="staff").startswith("$H$5")) - #default rounds - out of bounds - cc2 = CryptContext(policy=cc.policy.replace( - bsdi_crypt__default_rounds=35)) + # override scheme & custom settings self.assertEqual( - cc2.genconfig(scheme="bsdi_crypt", salt="nacl"), - '_S...nacl', + cc.genconfig(scheme="phpass", salt='.'*8, rounds=8, ident='P'), + '$P$6........', ) - # default+vary rounds - # this runs enough times the min and max *should* be hit, - # though there's a faint chance it will randomly fail. - from passlib.hash import bsdi_crypt as bc - cc3 = CryptContext(policy=cc.policy.replace( - bsdi_crypt__vary_rounds = 3)) - seen = set() - for i in xrange(3*2*50): - h = cc3.genconfig("bsdi_crypt", salt="nacl") - r = bc.from_string(h).rounds - seen.add(r) - self.assertTrue(min(seen)==22) - self.assertTrue(max(seen)==28) - - # default+vary % rounds - # this runs enough times the min and max *should* be hit, - # though there's a faint chance it will randomly fail. - from passlib.hash import sha256_crypt as sc - cc4 = CryptContext(policy=cc.policy.replace( - all__vary_rounds = "1%")) - seen = set() - for i in xrange(30*50): - h = cc4.genconfig(salt="nacl") - r = sc.from_string(h).rounds - seen.add(r) - self.assertTrue(min(seen)==2970) - self.assertTrue(max(seen)==3000) #NOTE: would be 3030, but clipped by max_rounds - - def test_11_encrypt_settings(self): - "test encrypt() honors policy settings" - cc = CryptContext(**self.sample_policy_1) + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # test unicode category strings are accepted under py2 + # this tests basic _get_record() used by encrypt/genhash/verify. + # we have to omit scheme=xxx so codepath is tested fully + if PY2: + c2 = cc.copy(default="phpass") + self.assertTrue(c2.genconfig(category=u("admin")).startswith("$P$5")) + self.assertTrue(c2.genconfig(category=u("staff")).startswith("$H$5")) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().genconfig) + self.assertRaises(KeyError, CryptContext().genconfig, scheme='md5_crypt') + + # bad scheme values + self.assertRaises(KeyError, cc.genconfig, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.genconfig, scheme=1, category='staff') + self.assertRaises(TypeError, cc.genconfig, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.genconfig, category=1) + + + def test_42_genhash(self): + "test genhash() method" + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + hash = cc.encrypt('stub') + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.genhash, secret, hash, **kwds) + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.genhash, 'secret', hash, **kwds) + + # .. but should accept None if default scheme lacks config string + cc = CryptContext(["mysql323"]) + self.assertIsInstance(cc.genhash("stub", None), str) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().genhash, 'secret', 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.genhash, 'secret', hash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.genhash, 'secret', hash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.genconfig, 'secret', hash, category=1) + + + def test_43_encrypt(self): + "test encrypt() method" + cc = CryptContext(**self.sample_4_dict) # hash specific settings self.assertEqual( - cc.encrypt("password", scheme="nthash"), - '$NT$8846f7eaee8fb117ad06bdd830b7586c', + cc.encrypt("password", scheme="phpass", salt='.'*8), + '$H$5........De04R5Egz0aq8Tf.1eVhY/', ) self.assertEqual( - cc.encrypt("password", scheme="nthash", ident="3"), - '$3$$8846f7eaee8fb117ad06bdd830b7586c', + cc.encrypt("password", scheme="phpass", salt='.'*8, ident="P"), + '$P$5........De04R5Egz0aq8Tf.1eVhY/', ) + # NOTE: more thorough job of rounds limits done below. + # min rounds - self.assertEqual( - cc.encrypt("password", rounds=1999, salt="nacl"), - '$5$rounds=2000$nacl$9/lTZ5nrfPuz8vphznnmHuDGFuvjSNvOEDsGmGfsS97', - ) - self.assertEqual( - cc.encrypt("password", rounds=2001, salt="nacl"), - '$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31' - ) + with self.assertWarningList(PasslibConfigWarning): + self.assertEqual( + cc.encrypt("password", rounds=1999, salt="nacl"), + '$5$rounds=2000$nacl$9/lTZ5nrfPuz8vphznnmHuDGFuvjSNvOEDsGmGfsS97', + ) - #TODO: - # max rounds - # default rounds - # falls back to max, then min. - # specified - # outside of min/max range - # default+vary rounds - # default+vary % rounds - - #make sure default > max doesn't cause error when vary is set - cc2 = cc.replace(sha256_crypt__default_rounds=4000) - with catch_warnings(): - warnings.filterwarnings("ignore", "vary default rounds: lower bound > upper bound.*", UserWarning) + with self.assertWarningList([]): self.assertEqual( - cc2.encrypt("password", salt="nacl"), - '$5$rounds=3000$nacl$oH831OVMbkl.Lbw1SXflly4dW8L3mSxpxDz1u1CK/B0', + cc.encrypt("password", rounds=2001, salt="nacl"), + '$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31' ) - def test_12_hash_needs_update(self): - "test hash_needs_update() method" - cc = CryptContext(**self.sample_policy_1) - - #check deprecated scheme - self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA')) - self.assertTrue(not cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) - - #check min rounds - self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) - self.assertTrue(not cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) - - #check max rounds - self.assertTrue(not cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) - self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) - - #========================================================= - #identify - #========================================================= - def test_20_basic(self): - "test basic encrypt/identify/verify functionality" - handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt] - cc = CryptContext(handlers, policy=None) + # NOTE: max rounds, etc tested in genconfig() - #run through handlers - for crypt in handlers: - h = cc.encrypt("test", scheme=crypt.name) - self.assertEqual(cc.identify(h), crypt.name) - self.assertEqual(cc.identify(h, resolve=True), crypt) - self.assertTrue(cc.verify('test', h)) - self.assertTrue(not cc.verify('notest', h)) + # make default > max throws error if attempted + self.assertRaises(ValueError, cc.copy, + sha256_crypt__default_rounds=4000) - #test default - h = cc.encrypt("test") - self.assertEqual(cc.identify(h), "md5_crypt") + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- - #test genhash - h = cc.genhash('secret', cc.genconfig()) - self.assertEqual(cc.identify(h), 'md5_crypt') + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.encrypt, secret, **kwds) - h = cc.genhash('secret', cc.genconfig(), scheme='md5_crypt') - self.assertEqual(cc.identify(h), 'md5_crypt') + # throws error without schemes + self.assertRaises(KeyError, CryptContext().encrypt, 'secret') + + # bad scheme values + self.assertRaises(KeyError, cc.encrypt, 'secret', scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.encrypt, 'secret', scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.encrypt, 'secret', category=1) - self.assertRaises(ValueError, cc.genhash, 'secret', cc.genconfig(), scheme="des_crypt") - def test_21_identify(self): + def test_44_identify(self): "test identify() border cases" handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] - cc = CryptContext(handlers, policy=None) + cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) - #check unknown hash + # check unknown hash self.assertEqual(cc.identify('$9$232323123$1287319827'), None) self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', required=True) - #make sure "None" is accepted - self.assertEqual(cc.identify(None), None) - self.assertRaises(ValueError, cc.identify, None, required=True) + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.identify, hash, **kwds) + + # throws error without schemes + cc = CryptContext() + self.assertIs(cc.identify('hash'), None) + self.assertRaises(KeyError, cc.identify, 'hash', required=True) - def test_22_verify(self): + # bad category values + self.assertRaises(TypeError, cc.identify, None, category=1) + + def test_45_verify(self): "test verify() scheme kwd" handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] - cc = CryptContext(handlers, policy=None) + cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) h = hash.md5_crypt.encrypt("test") - #check base verify + # check base verify self.assertTrue(cc.verify("test", h)) self.assertTrue(not cc.verify("notest", h)) - #check verify using right alg + # check verify using right alg self.assertTrue(cc.verify('test', h, scheme='md5_crypt')) self.assertTrue(not cc.verify('notest', h, scheme='md5_crypt')) - #check verify using wrong alg + # check verify using wrong alg self.assertRaises(ValueError, cc.verify, 'test', h, scheme='bsdi_crypt') - def test_23_verify_empty_hash(self): - "test verify() allows hash=None" - handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt] - cc = CryptContext(handlers, policy=None) - self.assertTrue(not cc.verify("test", None)) - for handler in handlers: - self.assertTrue(not cc.verify("test", None, scheme=handler.name)) + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # unknown hash should throw error + self.assertRaises(ValueError, cc.verify, 'stub', '$6$232323123$1287319827') + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + h = refhash = cc.encrypt('stub') + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.verify, secret, h, **kwds) + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for h, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.verify, 'secret', h, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().verify, 'secret', 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.verify, 'secret', refhash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.verify, 'secret', refhash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.verify, 'secret', refhash, category=1) + + def test_46_needs_update(self): + "test needs_update() method" + cc = CryptContext(**self.sample_4_dict) + + # check deprecated scheme + self.assertTrue(cc.needs_update('9XXD4trGYeGJA')) + self.assertFalse(cc.needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) + + # check min rounds + self.assertTrue(cc.needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) + self.assertFalse(cc.needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) + + # check max rounds + self.assertFalse(cc.needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) + self.assertTrue(cc.needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) + + #-------------------------------------------------------------- + # test _bind_needs_update() framework + #-------------------------------------------------------------- + bind_state = [] + check_state = [] + class dummy(uh.StaticHandler): + name = 'dummy' + _hash_prefix = '@' + + @classmethod + def _bind_needs_update(cls, **settings): + bind_state.append(settings) + return cls._needs_update + + @classmethod + def _needs_update(cls, hash, secret): + check_state.append((hash,secret)) + return secret == "nu" + + def _calc_checksum(self, secret): + from hashlib import md5 + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return str_to_uascii(md5(secret).hexdigest()) + + # creating context should call bind function w/ settings + ctx = CryptContext([dummy]) + self.assertEqual(bind_state, [{}]) + + # calling needs_update should query callback + hash = refhash = dummy.encrypt("test") + self.assertFalse(ctx.needs_update(hash)) + self.assertEqual(check_state, [(hash,None)]) + del check_state[:] + + # now with a password + self.assertFalse(ctx.needs_update(hash, secret='bob')) + self.assertEqual(check_state, [(hash,'bob')]) + del check_state[:] + + # now when it returns True + self.assertTrue(ctx.needs_update(hash, secret='nu')) + self.assertEqual(check_state, [(hash,'nu')]) + del check_state[:] + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.needs_update, hash, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().needs_update, 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.needs_update, refhash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.needs_update, refhash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.needs_update, refhash, category=1) + + def test_47_verify_and_update(self): + "test verify_and_update()" + cc = CryptContext(**self.sample_4_dict) + + # create some hashes + h1 = cc.encrypt("password", scheme="des_crypt") + h2 = cc.encrypt("password", scheme="sha256_crypt") + + # check bad password, deprecated hash + ok, new_hash = cc.verify_and_update("wrongpass", h1) + self.assertFalse(ok) + self.assertIs(new_hash, None) + + # check bad password, good hash + ok, new_hash = cc.verify_and_update("wrongpass", h2) + self.assertFalse(ok) + self.assertIs(new_hash, None) + + # check right password, deprecated hash + ok, new_hash = cc.verify_and_update("password", h1) + self.assertTrue(ok) + self.assertTrue(cc.identify(new_hash), "sha256_crypt") + + # check right password, good hash + ok, new_hash = cc.verify_and_update("password", h2) + self.assertTrue(ok) + self.assertIs(new_hash, None) + + #-------------------------------------------------------------- + # border cases + #-------------------------------------------------------------- + + # rejects non-string secrets + cc = CryptContext(["des_crypt"]) + hash = refhash = cc.encrypt('stub') + for secret, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.verify_and_update, secret, hash, **kwds) + + # rejects non-string hashes + cc = CryptContext(["des_crypt"]) + for hash, kwds in self.nonstring_vectors: + self.assertRaises(TypeError, cc.verify_and_update, 'secret', hash, **kwds) + + # throws error without schemes + self.assertRaises(KeyError, CryptContext().verify_and_update, 'secret', 'hash') + + # bad scheme values + self.assertRaises(KeyError, cc.verify_and_update, 'secret', refhash, scheme="fake") # XXX: should this be ValueError? + self.assertRaises(TypeError, cc.verify_and_update, 'secret', refhash, scheme=1) + + # bad category values + self.assertRaises(TypeError, cc.verify_and_update, 'secret', refhash, category=1) + + #=================================================================== + # rounds options + #=================================================================== + # NOTE: the follow tests check how _CryptRecord handles + # the min/max/default/vary_rounds options, via the output of + # genconfig(). it's assumed encrypt() takes the same codepath. + + def test_50_rounds_limits(self): + "test rounds limits" + cc = CryptContext(schemes=["sha256_crypt"], + all__min_rounds=2000, + all__max_rounds=3000, + all__default_rounds=2500, + ) + + #-------------------------------------------------- + # min_rounds + #-------------------------------------------------- + + # set below handler minimum + with self.assertWarningList([PasslibConfigWarning]*2): + c2 = cc.copy(all__min_rounds=500, all__max_rounds=None, + all__default_rounds=500) + self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$") - def test_24_min_verify_time(self): + # below policy minimum + with self.assertWarningList(PasslibConfigWarning): + self.assertEqual( + cc.genconfig(rounds=1999, salt="nacl"), + '$5$rounds=2000$nacl$', + ) + + # equal to policy minimum + self.assertEqual( + cc.genconfig(rounds=2000, salt="nacl"), + '$5$rounds=2000$nacl$', + ) + + # above policy minimum + self.assertEqual( + cc.genconfig(rounds=2001, salt="nacl"), + '$5$rounds=2001$nacl$' + ) + + #-------------------------------------------------- + # max rounds + #-------------------------------------------------- + + # set above handler max + with self.assertWarningList([PasslibConfigWarning]*2): + c2 = cc.copy(all__max_rounds=int(1e9)+500, all__min_rounds=None, + all__default_rounds=int(1e9)+500) + + self.assertEqual(c2.genconfig(salt="nacl"), + "$5$rounds=999999999$nacl$") + + # above policy max + with self.assertWarningList(PasslibConfigWarning): + self.assertEqual( + cc.genconfig(rounds=3001, salt="nacl"), + '$5$rounds=3000$nacl$' + ) + + # equal policy max + self.assertEqual( + cc.genconfig(rounds=3000, salt="nacl"), + '$5$rounds=3000$nacl$' + ) + + # below policy max + self.assertEqual( + cc.genconfig(rounds=2999, salt="nacl"), + '$5$rounds=2999$nacl$', + ) + + #-------------------------------------------------- + # default_rounds + #-------------------------------------------------- + + # explicit default rounds + self.assertEqual(cc.genconfig(salt="nacl"), '$5$rounds=2500$nacl$') + + # fallback default rounds - use handler's + df = hash.sha256_crypt.default_rounds + c2 = cc.copy(all__default_rounds=None, all__max_rounds=df<<1) + self.assertEqual(c2.genconfig(salt="nacl"), + '$5$rounds=%d$nacl$' % df) + + # fallback default rounds - use handler's, but clipped to max rounds + c2 = cc.copy(all__default_rounds=None, all__max_rounds=3000) + self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=3000$nacl$') + + # TODO: test default falls back to mx / mn if handler has no default. + + # default rounds - out of bounds + self.assertRaises(ValueError, cc.copy, all__default_rounds=1999) + cc.copy(all__default_rounds=2000) + cc.copy(all__default_rounds=3000) + self.assertRaises(ValueError, cc.copy, all__default_rounds=3001) + + #-------------------------------------------------- + # border cases + #-------------------------------------------------- + + # invalid min/max bounds + c2 = CryptContext(schemes=["sha256_crypt"]) + self.assertRaises(ValueError, c2.copy, all__min_rounds=-1) + self.assertRaises(ValueError, c2.copy, all__max_rounds=-1) + self.assertRaises(ValueError, c2.copy, all__min_rounds=2000, + all__max_rounds=1999) + + # test bad values + self.assertRaises(ValueError, CryptContext, all__min_rounds='x') + self.assertRaises(ValueError, CryptContext, all__max_rounds='x') + self.assertRaises(ValueError, CryptContext, all__vary_rounds='x') + self.assertRaises(ValueError, CryptContext, all__default_rounds='x') + + # test bad types rejected + bad = NotImplemented + self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__min_rounds=bad) + self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__max_rounds=bad) + self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__vary_rounds=bad) + self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__default_rounds=bad) + + def test_51_linear_vary_rounds(self): + "test linear vary rounds" + cc = CryptContext(schemes=["sha256_crypt"], + all__min_rounds=1995, + all__max_rounds=2005, + all__default_rounds=2000, + ) + + # test negative + self.assertRaises(ValueError, cc.copy, all__vary_rounds=-1) + self.assertRaises(ValueError, cc.copy, all__vary_rounds="-1%") + self.assertRaises(ValueError, cc.copy, all__vary_rounds="101%") + + # test static + c2 = cc.copy(all__vary_rounds=0) + self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000) + + c2 = cc.copy(all__vary_rounds="0%") + self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000) + + # test absolute + c2 = cc.copy(all__vary_rounds=1) + self.assert_rounds_range(c2, "sha256_crypt", 1999, 2001) + c2 = cc.copy(all__vary_rounds=100) + self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005) + + # test relative + c2 = cc.copy(all__vary_rounds="0.1%") + self.assert_rounds_range(c2, "sha256_crypt", 1998, 2002) + c2 = cc.copy(all__vary_rounds="100%") + self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005) + + def test_52_log2_vary_rounds(self): + "test log2 vary rounds" + cc = CryptContext(schemes=["bcrypt"], + all__min_rounds=15, + all__max_rounds=25, + all__default_rounds=20, + ) + + # test negative + self.assertRaises(ValueError, cc.copy, all__vary_rounds=-1) + self.assertRaises(ValueError, cc.copy, all__vary_rounds="-1%") + self.assertRaises(ValueError, cc.copy, all__vary_rounds="101%") + + # test static + c2 = cc.copy(all__vary_rounds=0) + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + c2 = cc.copy(all__vary_rounds="0%") + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + # test absolute + c2 = cc.copy(all__vary_rounds=1) + self.assert_rounds_range(c2, "bcrypt", 19, 21) + c2 = cc.copy(all__vary_rounds=100) + self.assert_rounds_range(c2, "bcrypt", 15, 25) + + # test relative - should shift over at 50% mark + c2 = cc.copy(all__vary_rounds="1%") + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + c2 = cc.copy(all__vary_rounds="49%") + self.assert_rounds_range(c2, "bcrypt", 20, 20) + + c2 = cc.copy(all__vary_rounds="50%") + self.assert_rounds_range(c2, "bcrypt", 19, 20) + + c2 = cc.copy(all__vary_rounds="100%") + self.assert_rounds_range(c2, "bcrypt", 15, 21) + + def assert_rounds_range(self, context, scheme, lower, upper): + "helper to check vary_rounds covers specified range" + # NOTE: this runs enough times the min and max *should* be hit, + # though there's a faint chance it will randomly fail. + handler = context.handler(scheme) + salt = handler.default_salt_chars[0:1] * handler.max_salt_size + seen = set() + for i in irange(300): + h = context.genconfig(scheme, salt=salt) + r = handler.from_string(h).rounds + seen.add(r) + self.assertEqual(min(seen), lower, "vary_rounds had wrong lower limit:") + self.assertEqual(max(seen), upper, "vary_rounds had wrong upper limit:") + + #=================================================================== + # feature tests + #=================================================================== + def test_60_min_verify_time(self): "test verify() honors min_verify_time" - #NOTE: this whole test assumes time.sleep() and time.time() - # have at least 1ms accuracy - delta = .1 - min_delay = delta - min_verify_time = min_delay + 2*delta - max_delay = min_verify_time + 2*delta + delta = .05 + if TICK_RESOLUTION >= delta/10: + raise self.skipTest("timer not accurate enough") + min_delay = 2*delta + min_verify_time = 5*delta + max_delay = 8*delta class TimedHash(uh.StaticHandler): "psuedo hash that takes specified amount of time" @@ -802,111 +1449,146 @@ def identify(cls, hash): return True - @classmethod - def genhash(cls, secret, hash): - time.sleep(cls.delay) - return hash or 'x' - - cc = CryptContext([TimedHash], min_verify_time=min_verify_time) + def _calc_checksum(self, secret): + quicksleep(self.delay) + return to_unicode(secret + 'x') + + # check mvt issues a warning, and then filter for remainder of test + with self.assertWarningList(["'min_verify_time' is deprecated"]*2): + cc = CryptContext([TimedHash], min_verify_time=min_verify_time, + admin__context__min_verify_time=min_verify_time*2) + warnings.filterwarnings("ignore", "'min_verify_time' is deprecated") def timecall(func, *args, **kwds): - start = time.time() + start = tick() result = func(*args, **kwds) - end = time.time() - return end-start, result + return tick()-start, result - #verify hashing works + # verify genhash delay works TimedHash.delay = min_delay - elapsed, _ = timecall(TimedHash.genhash, 'stub', 'stub') + elapsed, result = timecall(TimedHash.genhash, 'stub', None) + self.assertEqual(result, 'stubx') self.assertAlmostEqual(elapsed, min_delay, delta=delta) - #ensure min verify time is honored - elapsed, _ = timecall(cc.verify, "stub", "stub") + # ensure min verify time is honored + + # correct password + elapsed, result = timecall(cc.verify, "stub", "stubx") + self.assertTrue(result) + self.assertAlmostEqual(elapsed, min_delay, delta=delta) + + # incorrect password + elapsed, result = timecall(cc.verify, "blob", "stubx") + self.assertFalse(result) self.assertAlmostEqual(elapsed, min_verify_time, delta=delta) - #ensure taking longer emits a warning. + # incorrect password w/ special category setting + elapsed, result = timecall(cc.verify, "blob", "stubx", category="admin") + self.assertFalse(result) + self.assertAlmostEqual(elapsed, min_verify_time*2, delta=delta) + + # ensure taking longer emits a warning. TimedHash.delay = max_delay - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") - elapsed, _ = timecall(cc.verify, "stub", "stub") + with self.assertWarningList(".*verify exceeded min_verify_time"): + elapsed, result = timecall(cc.verify, "blob", "stubx") + self.assertFalse(result) self.assertAlmostEqual(elapsed, max_delay, delta=delta) - self.assertEqual(len(wlog), 1) - self.assertWarningMatches(wlog[0], - message_re="CryptContext: verify exceeded min_verify_time") - def test_25_verify_and_update(self): - "test verify_and_update()" - cc = CryptContext(**self.sample_policy_1) + # reject values < 0 + self.assertRaises(ValueError, CryptContext, min_verify_time=-1) - #create some hashes - h1 = cc.encrypt("password", scheme="des_crypt") - h2 = cc.encrypt("password", scheme="sha256_crypt") + def test_61_autodeprecate(self): + "test deprecated='auto' is handled correctly" - #check bad password, deprecated hash - ok, new_hash = cc.verify_and_update("wrongpass", h1) - self.assertFalse(ok) - self.assertIs(new_hash, None) + def getstate(ctx, category=None): + return [ctx._is_deprecated_scheme(scheme, category) for scheme in ctx.schemes()] - #check bad password, good hash - ok, new_hash = cc.verify_and_update("wrongpass", h2) - self.assertFalse(ok) - self.assertIs(new_hash, None) - - #check right password, deprecated hash - ok, new_hash = cc.verify_and_update("password", h1) - self.assertTrue(ok) - self.assertTrue(cc.identify(new_hash), "sha256_crypt") + # correctly reports default + ctx = CryptContext("sha256_crypt,md5_crypt,des_crypt", deprecated="auto") + self.assertEqual(getstate(ctx, None), [False, True, True]) + self.assertEqual(getstate(ctx, "admin"), [False, True, True]) + + # correctly reports changed default + ctx.update(default="md5_crypt") + self.assertEqual(getstate(ctx, None), [True, False, True]) + self.assertEqual(getstate(ctx, "admin"), [True, False, True]) + + # category default is handled correctly + ctx.update(admin__context__default="des_crypt") + self.assertEqual(getstate(ctx, None), [True, False, True]) + self.assertEqual(getstate(ctx, "admin"), [True, True, False]) + + # handles 1 scheme + ctx = CryptContext(["sha256_crypt"], deprecated="auto") + self.assertEqual(getstate(ctx, None), [False]) + self.assertEqual(getstate(ctx, "admin"), [False]) + + # disallow auto & other deprecated schemes at same time. + self.assertRaises(ValueError, CryptContext, "sha256_crypt,md5_crypt", + deprecated="auto,md5_crypt") + self.assertRaises(ValueError, CryptContext, "sha256_crypt,md5_crypt", + deprecated="md5_crypt,auto") + + #=================================================================== + # handler deprecation detectors + #=================================================================== + def test_62_bcrypt_update(self): + "test verify_and_update / needs_update corrects bcrypt padding" + # see issue 25. + bcrypt = hash.bcrypt - #check right password, good hash - ok, new_hash = cc.verify_and_update("password", h2) + PASS1 = "test" + BAD1 = "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" + GOOD1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" + ctx = CryptContext(["bcrypt"], bcrypt__rounds=4) + + self.assertTrue(ctx.needs_update(BAD1)) + self.assertFalse(ctx.needs_update(GOOD1)) + + if bcrypt.has_backend(): + self.assertEqual(ctx.verify_and_update(PASS1,GOOD1), (True,None)) + with self.assertWarningList(["incorrect.*padding bits"]*2): + self.assertEqual(ctx.verify_and_update("x",BAD1), (False,None)) + ok, new_hash = ctx.verify_and_update(PASS1, BAD1) + self.assertTrue(ok) + self.assertTrue(new_hash and new_hash != BAD1) + + def test_63_bsdi_crypt_update(self): + "test verify_and_update / needs_update corrects bsdi even rounds" + even_hash = '_Y/../cG0zkJa6LY6k4c' + odd_hash = '_Z/..TgFg0/ptQtpAgws' + secret = 'test' + ctx = CryptContext(['bsdi_crypt'], bsdi_crypt__min_rounds=5) + + self.assertTrue(ctx.needs_update(even_hash)) + self.assertFalse(ctx.needs_update(odd_hash)) + + self.assertEqual(ctx.verify_and_update(secret, odd_hash), (True,None)) + self.assertEqual(ctx.verify_and_update("x", even_hash), (False,None)) + ok, new_hash = ctx.verify_and_update(secret, even_hash) self.assertTrue(ok) - self.assertIs(new_hash, None) + self.assertTrue(new_hash and new_hash != even_hash) - #========================================================= - # other - #========================================================= - def test_90_bcrypt_normhash(self): - "teset verify_and_update / hash_needs_update corrects bcrypt padding" - # see issue 25. - bcrypt = hash.bcrypt - - PASS1 = "loppux" - BAD1 = "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C" - GOOD1 = "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C" - ctx = CryptContext(["bcrypt"]) - - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") - - self.assertTrue(ctx.hash_needs_update(BAD1)) - self.assertFalse(ctx.hash_needs_update(GOOD1)) - - if bcrypt.has_backend(): - self.assertEquals(ctx.verify_and_update(PASS1,GOOD1), (True,None)) - self.assertEquals(ctx.verify_and_update("x",BAD1), (False,None)) - res = ctx.verify_and_update(PASS1, BAD1) - self.assertTrue(res[0] and res[1] and res[1] != BAD1) - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#LazyCryptContext -#========================================================= + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# LazyCryptContext +#============================================================================= class dummy_2(uh.StaticHandler): name = "dummy_2" class LazyCryptContextTest(TestCase): - case_prefix = "LazyCryptContext" + descriptionPrefix = "LazyCryptContext" def setUp(self): + # make sure this isn't registered before OR after unload_handler_name("dummy_2") - - def tearDown(self): - unload_handler_name("dummy_2") + self.addCleanup(unload_handler_name, "dummy_2") def test_kwd_constructor(self): + "test plain kwds" self.assertFalse(has_crypt_handler("dummy_2")) register_crypt_handler_path("dummy_2", "passlib.tests.test_context") @@ -914,8 +1596,8 @@ self.assertFalse(has_crypt_handler("dummy_2", True)) - self.assertTrue(cc.policy.handler_is_deprecated("des_crypt")) - self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"]) + self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt")) + self.assertTrue(cc._is_deprecated_scheme("des_crypt")) self.assertTrue(has_crypt_handler("dummy_2", True)) @@ -923,19 +1605,19 @@ self.assertFalse(has_crypt_handler("dummy_2")) register_crypt_handler_path("dummy_2", "passlib.tests.test_context") - def create_policy(flag=False): + def onload(flag=False): self.assertTrue(flag) - return CryptPolicy(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + return dict(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) - cc = LazyCryptContext(create_policy=create_policy, flag=True) + cc = LazyCryptContext(onload=onload, flag=True) self.assertFalse(has_crypt_handler("dummy_2", True)) - self.assertTrue(cc.policy.handler_is_deprecated("des_crypt")) - self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"]) + self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt")) + self.assertTrue(cc._is_deprecated_scheme("des_crypt")) self.assertTrue(has_crypt_handler("dummy_2", True)) -#========================================================= -#EOF -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_context_deprecated.py passlib-1.6.1/passlib/tests/test_context_deprecated.py --- passlib-1.5.3/passlib/tests/test_context_deprecated.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_context_deprecated.py 2012-08-02 03:27:36.000000000 +0000 @@ -0,0 +1,752 @@ +"""tests for passlib.context + +this file is a clone of the 1.5 test_context.py, +containing the tests using the legacy CryptPolicy api. +it's being preserved here to ensure the old api doesn't break +(until Passlib 1.8, when this and the legacy api will be removed). +""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import hashlib +from logging import getLogger +import os +import time +import warnings +import sys +# site +try: + from pkg_resources import resource_filename +except ImportError: + resource_filename = None +# pkg +from passlib import hash +from passlib.context import CryptContext, CryptPolicy, LazyCryptContext +from passlib.exc import PasslibConfigWarning +from passlib.utils import tick, to_bytes, to_unicode +from passlib.utils.compat import irange, u, bytes +import passlib.utils.handlers as uh +from passlib.tests.utils import TestCase, catch_warnings, set_file +from passlib.registry import (register_crypt_handler_path, + _has_crypt_handler as has_crypt_handler, + _unload_handler_name as unload_handler_name, + get_crypt_handler, + ) +# module +log = getLogger(__name__) + +#============================================================================= +# +#============================================================================= +class CryptPolicyTest(TestCase): + "test CryptPolicy object" + + # TODO: need to test user categories w/in all this + + descriptionPrefix = "CryptPolicy" + + #=================================================================== + # sample crypt policies used for testing + #=================================================================== + + #--------------------------------------------------------------- + # sample 1 - average config file + #--------------------------------------------------------------- + # NOTE: copy of this is stored in file passlib/tests/sample_config_1s.cfg + sample_config_1s = """\ +[passlib] +schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt +default = md5_crypt +all.vary_rounds = 10%% +bsdi_crypt.max_rounds = 30000 +bsdi_crypt.default_rounds = 25000 +sha512_crypt.max_rounds = 50000 +sha512_crypt.min_rounds = 40000 +""" + sample_config_1s_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), "sample_config_1s.cfg")) + if not os.path.exists(sample_config_1s_path) and resource_filename: + # in case we're zipped up in an egg. + sample_config_1s_path = resource_filename("passlib.tests", + "sample_config_1s.cfg") + + # make sure sample_config_1s uses \n linesep - tests rely on this + assert sample_config_1s.startswith("[passlib]\nschemes") + + sample_config_1pd = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + default = "md5_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__max_rounds = 30000, + bsdi_crypt__default_rounds = 25000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds = 40000, + ) + + sample_config_1pid = { + "schemes": "des_crypt, md5_crypt, bsdi_crypt, sha512_crypt", + "default": "md5_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + "all.vary_rounds": 0.1, + "bsdi_crypt.max_rounds": 30000, + "bsdi_crypt.default_rounds": 25000, + "sha512_crypt.max_rounds": 50000, + "sha512_crypt.min_rounds": 40000, + } + + sample_config_1prd = dict( + schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt], + default = "md5_crypt", # NOTE: passlib <= 1.5 was handler obj. + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__max_rounds = 30000, + bsdi_crypt__default_rounds = 25000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds = 40000, + ) + + #--------------------------------------------------------------- + # sample 2 - partial policy & result of overlay on sample 1 + #--------------------------------------------------------------- + sample_config_2s = """\ +[passlib] +bsdi_crypt.min_rounds = 29000 +bsdi_crypt.max_rounds = 35000 +bsdi_crypt.default_rounds = 31000 +sha512_crypt.min_rounds = 45000 +""" + + sample_config_2pd = dict( + # using this to test full replacement of existing options + bsdi_crypt__min_rounds = 29000, + bsdi_crypt__max_rounds = 35000, + bsdi_crypt__default_rounds = 31000, + # using this to test partial replacement of existing options + sha512_crypt__min_rounds=45000, + ) + + sample_config_12pd = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + default = "md5_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__min_rounds = 29000, + bsdi_crypt__max_rounds = 35000, + bsdi_crypt__default_rounds = 31000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds=45000, + ) + + #--------------------------------------------------------------- + # sample 3 - just changing default + #--------------------------------------------------------------- + sample_config_3pd = dict( + default="sha512_crypt", + ) + + sample_config_123pd = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + default = "sha512_crypt", + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + bsdi_crypt__min_rounds = 29000, + bsdi_crypt__max_rounds = 35000, + bsdi_crypt__default_rounds = 31000, + sha512_crypt__max_rounds = 50000, + sha512_crypt__min_rounds=45000, + ) + + #--------------------------------------------------------------- + # sample 4 - category specific + #--------------------------------------------------------------- + sample_config_4s = """ +[passlib] +schemes = sha512_crypt +all.vary_rounds = 10%% +default.sha512_crypt.max_rounds = 20000 +admin.all.vary_rounds = 5%% +admin.sha512_crypt.max_rounds = 40000 +""" + + sample_config_4pd = dict( + schemes = [ "sha512_crypt" ], + # NOTE: not maintaining backwards compat for rendering to "10%" + all__vary_rounds = 0.1, + sha512_crypt__max_rounds = 20000, + # NOTE: not maintaining backwards compat for rendering to "5%" + admin__all__vary_rounds = 0.05, + admin__sha512_crypt__max_rounds = 40000, + ) + + #--------------------------------------------------------------- + # sample 5 - to_string & deprecation testing + #--------------------------------------------------------------- + sample_config_5s = sample_config_1s + """\ +deprecated = des_crypt +admin__context__deprecated = des_crypt, bsdi_crypt +""" + + sample_config_5pd = sample_config_1pd.copy() + sample_config_5pd.update( + deprecated = [ "des_crypt" ], + admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ], + ) + + sample_config_5pid = sample_config_1pid.copy() + sample_config_5pid.update({ + "deprecated": "des_crypt", + "admin.context.deprecated": "des_crypt, bsdi_crypt", + }) + + sample_config_5prd = sample_config_1prd.copy() + sample_config_5prd.update({ + # XXX: should deprecated return the actual handlers in this case? + # would have to modify how policy stores info, for one. + "deprecated": ["des_crypt"], + "admin__context__deprecated": ["des_crypt", "bsdi_crypt"], + }) + + #=================================================================== + # constructors + #=================================================================== + def setUp(self): + TestCase.setUp(self) + warnings.filterwarnings("ignore", + r"The CryptPolicy class has been deprecated") + warnings.filterwarnings("ignore", + r"the method.*hash_needs_update.*is deprecated") + + def test_00_constructor(self): + "test CryptPolicy() constructor" + policy = CryptPolicy(**self.sample_config_1pd) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + policy = CryptPolicy(self.sample_config_1pd) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + self.assertRaises(TypeError, CryptPolicy, {}, {}) + self.assertRaises(TypeError, CryptPolicy, {}, dummy=1) + + # check key with too many separators is rejected + self.assertRaises(TypeError, CryptPolicy, + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"], + bad__key__bsdi_crypt__max_rounds = 30000, + ) + + # check nameless handler rejected + class nameless(uh.StaticHandler): + name = None + self.assertRaises(ValueError, CryptPolicy, schemes=[nameless]) + + # check scheme must be name or crypt handler + self.assertRaises(TypeError, CryptPolicy, schemes=[uh.StaticHandler]) + + # check name conflicts are rejected + class dummy_1(uh.StaticHandler): + name = 'dummy_1' + self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1]) + + # with unknown deprecated value + self.assertRaises(KeyError, CryptPolicy, + schemes=['des_crypt'], + deprecated=['md5_crypt']) + + # with unknown default value + self.assertRaises(KeyError, CryptPolicy, + schemes=['des_crypt'], + default='md5_crypt') + + def test_01_from_path_simple(self): + "test CryptPolicy.from_path() constructor" + # NOTE: this is separate so it can also run under GAE + + # test preset stored in existing file + path = self.sample_config_1s_path + policy = CryptPolicy.from_path(path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test if path missing + self.assertRaises(EnvironmentError, CryptPolicy.from_path, path + 'xxx') + + def test_01_from_path(self): + "test CryptPolicy.from_path() constructor with encodings" + path = self.mktemp() + + # test "\n" linesep + set_file(path, self.sample_config_1s) + policy = CryptPolicy.from_path(path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test "\r\n" linesep + set_file(path, self.sample_config_1s.replace("\n","\r\n")) + policy = CryptPolicy.from_path(path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test with custom encoding + uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8") + set_file(path, uc2) + policy = CryptPolicy.from_path(path, encoding="utf-16") + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + def test_02_from_string(self): + "test CryptPolicy.from_string() constructor" + # test "\n" linesep + policy = CryptPolicy.from_string(self.sample_config_1s) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test "\r\n" linesep + policy = CryptPolicy.from_string( + self.sample_config_1s.replace("\n","\r\n")) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test with unicode + data = to_unicode(self.sample_config_1s) + policy = CryptPolicy.from_string(data) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test with non-ascii-compatible encoding + uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8") + policy = CryptPolicy.from_string(uc2, encoding="utf-16") + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # test category specific options + policy = CryptPolicy.from_string(self.sample_config_4s) + self.assertEqual(policy.to_dict(), self.sample_config_4pd) + + def test_03_from_source(self): + "test CryptPolicy.from_source() constructor" + # pass it a path + policy = CryptPolicy.from_source(self.sample_config_1s_path) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass it a string + policy = CryptPolicy.from_source(self.sample_config_1s) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass it a dict (NOTE: make a copy to detect in-place modifications) + policy = CryptPolicy.from_source(self.sample_config_1pd.copy()) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass it existing policy + p2 = CryptPolicy.from_source(policy) + self.assertIs(policy, p2) + + # pass it something wrong + self.assertRaises(TypeError, CryptPolicy.from_source, 1) + self.assertRaises(TypeError, CryptPolicy.from_source, []) + + def test_04_from_sources(self): + "test CryptPolicy.from_sources() constructor" + + # pass it empty list + self.assertRaises(ValueError, CryptPolicy.from_sources, []) + + # pass it one-element list + policy = CryptPolicy.from_sources([self.sample_config_1s]) + self.assertEqual(policy.to_dict(), self.sample_config_1pd) + + # pass multiple sources + policy = CryptPolicy.from_sources( + [ + self.sample_config_1s_path, + self.sample_config_2s, + self.sample_config_3pd, + ]) + self.assertEqual(policy.to_dict(), self.sample_config_123pd) + + def test_05_replace(self): + "test CryptPolicy.replace() constructor" + + p1 = CryptPolicy(**self.sample_config_1pd) + + # check overlaying sample 2 + p2 = p1.replace(**self.sample_config_2pd) + self.assertEqual(p2.to_dict(), self.sample_config_12pd) + + # check repeating overlay makes no change + p2b = p2.replace(**self.sample_config_2pd) + self.assertEqual(p2b.to_dict(), self.sample_config_12pd) + + # check overlaying sample 3 + p3 = p2.replace(self.sample_config_3pd) + self.assertEqual(p3.to_dict(), self.sample_config_123pd) + + def test_06_forbidden(self): + "test CryptPolicy() forbidden kwds" + + # salt not allowed to be set + self.assertRaises(KeyError, CryptPolicy, + schemes=["des_crypt"], + des_crypt__salt="xx", + ) + self.assertRaises(KeyError, CryptPolicy, + schemes=["des_crypt"], + all__salt="xx", + ) + + # schemes not allowed for category + self.assertRaises(KeyError, CryptPolicy, + schemes=["des_crypt"], + user__context__schemes=["md5_crypt"], + ) + + #=================================================================== + # reading + #=================================================================== + def test_10_has_schemes(self): + "test has_schemes() method" + + p1 = CryptPolicy(**self.sample_config_1pd) + self.assertTrue(p1.has_schemes()) + + p3 = CryptPolicy(**self.sample_config_3pd) + self.assertTrue(not p3.has_schemes()) + + def test_11_iter_handlers(self): + "test iter_handlers() method" + + p1 = CryptPolicy(**self.sample_config_1pd) + s = self.sample_config_1prd['schemes'] + self.assertEqual(list(p1.iter_handlers()), s) + + p3 = CryptPolicy(**self.sample_config_3pd) + self.assertEqual(list(p3.iter_handlers()), []) + + def test_12_get_handler(self): + "test get_handler() method" + + p1 = CryptPolicy(**self.sample_config_1pd) + + # check by name + self.assertIs(p1.get_handler("bsdi_crypt"), hash.bsdi_crypt) + + # check by missing name + self.assertIs(p1.get_handler("sha256_crypt"), None) + self.assertRaises(KeyError, p1.get_handler, "sha256_crypt", required=True) + + # check default + self.assertIs(p1.get_handler(), hash.md5_crypt) + + def test_13_get_options(self): + "test get_options() method" + + p12 = CryptPolicy(**self.sample_config_12pd) + + self.assertEqual(p12.get_options("bsdi_crypt"),dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds = 0.1, + min_rounds = 29000, + max_rounds = 35000, + default_rounds = 31000, + )) + + self.assertEqual(p12.get_options("sha512_crypt"),dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds = 0.1, + min_rounds = 45000, + max_rounds = 50000, + )) + + p4 = CryptPolicy.from_string(self.sample_config_4s) + self.assertEqual(p4.get_options("sha512_crypt"), dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds=0.1, + max_rounds=20000, + )) + + self.assertEqual(p4.get_options("sha512_crypt", "user"), dict( + # NOTE: not maintaining backwards compat for rendering to "10%" + vary_rounds=0.1, + max_rounds=20000, + )) + + self.assertEqual(p4.get_options("sha512_crypt", "admin"), dict( + # NOTE: not maintaining backwards compat for rendering to "5%" + vary_rounds=0.05, + max_rounds=40000, + )) + + def test_14_handler_is_deprecated(self): + "test handler_is_deprecated() method" + pa = CryptPolicy(**self.sample_config_1pd) + pb = CryptPolicy(**self.sample_config_5pd) + + self.assertFalse(pa.handler_is_deprecated("des_crypt")) + self.assertFalse(pa.handler_is_deprecated(hash.bsdi_crypt)) + self.assertFalse(pa.handler_is_deprecated("sha512_crypt")) + + self.assertTrue(pb.handler_is_deprecated("des_crypt")) + self.assertFalse(pb.handler_is_deprecated(hash.bsdi_crypt)) + self.assertFalse(pb.handler_is_deprecated("sha512_crypt")) + + # check categories as well + self.assertTrue(pb.handler_is_deprecated("des_crypt", "user")) + self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "user")) + self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin")) + self.assertTrue(pb.handler_is_deprecated("bsdi_crypt", "admin")) + + # check deprecation is overridden per category + pc = CryptPolicy( + schemes=["md5_crypt", "des_crypt"], + deprecated=["md5_crypt"], + user__context__deprecated=["des_crypt"], + ) + self.assertTrue(pc.handler_is_deprecated("md5_crypt")) + self.assertFalse(pc.handler_is_deprecated("des_crypt")) + self.assertFalse(pc.handler_is_deprecated("md5_crypt", "user")) + self.assertTrue(pc.handler_is_deprecated("des_crypt", "user")) + + def test_15_min_verify_time(self): + "test get_min_verify_time() method" + # silence deprecation warnings for min verify time + warnings.filterwarnings("ignore", category=DeprecationWarning) + + pa = CryptPolicy() + self.assertEqual(pa.get_min_verify_time(), 0) + self.assertEqual(pa.get_min_verify_time('admin'), 0) + + pb = pa.replace(min_verify_time=.1) + self.assertEqual(pb.get_min_verify_time(), .1) + self.assertEqual(pb.get_min_verify_time('admin'), .1) + + pc = pa.replace(admin__context__min_verify_time=.2) + self.assertEqual(pc.get_min_verify_time(), 0) + self.assertEqual(pc.get_min_verify_time('admin'), .2) + + pd = pb.replace(admin__context__min_verify_time=.2) + self.assertEqual(pd.get_min_verify_time(), .1) + self.assertEqual(pd.get_min_verify_time('admin'), .2) + + #=================================================================== + # serialization + #=================================================================== + def test_20_iter_config(self): + "test iter_config() method" + p5 = CryptPolicy(**self.sample_config_5pd) + self.assertEqual(dict(p5.iter_config()), self.sample_config_5pd) + self.assertEqual(dict(p5.iter_config(resolve=True)), self.sample_config_5prd) + self.assertEqual(dict(p5.iter_config(ini=True)), self.sample_config_5pid) + + def test_21_to_dict(self): + "test to_dict() method" + p5 = CryptPolicy(**self.sample_config_5pd) + self.assertEqual(p5.to_dict(), self.sample_config_5pd) + self.assertEqual(p5.to_dict(resolve=True), self.sample_config_5prd) + + def test_22_to_string(self): + "test to_string() method" + pa = CryptPolicy(**self.sample_config_5pd) + s = pa.to_string() # NOTE: can't compare string directly, ordering etc may not match + pb = CryptPolicy.from_string(s) + self.assertEqual(pb.to_dict(), self.sample_config_5pd) + + s = pa.to_string(encoding="latin-1") + self.assertIsInstance(s, bytes) + + #=================================================================== + # + #=================================================================== + +#============================================================================= +# CryptContext +#============================================================================= +class CryptContextTest(TestCase): + "test CryptContext class" + descriptionPrefix = "CryptContext" + + def setUp(self): + TestCase.setUp(self) + warnings.filterwarnings("ignore", + r"CryptContext\(\)\.replace\(\) has been deprecated.*") + warnings.filterwarnings("ignore", + r"The CryptContext ``policy`` keyword has been deprecated.*") + warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*") + warnings.filterwarnings("ignore", + r"the method.*hash_needs_update.*is deprecated") + + #=================================================================== + # constructor + #=================================================================== + def test_00_constructor(self): + "test constructor" + # create crypt context using handlers + cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt]) + c,b,a = cc.policy.iter_handlers() + self.assertIs(a, hash.des_crypt) + self.assertIs(b, hash.bsdi_crypt) + self.assertIs(c, hash.md5_crypt) + + # create context using names + cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) + c,b,a = cc.policy.iter_handlers() + self.assertIs(a, hash.des_crypt) + self.assertIs(b, hash.bsdi_crypt) + self.assertIs(c, hash.md5_crypt) + + # policy kwd + policy = cc.policy + cc = CryptContext(policy=policy) + self.assertEqual(cc.to_dict(), policy.to_dict()) + + cc = CryptContext(policy=policy, default="bsdi_crypt") + self.assertNotEqual(cc.to_dict(), policy.to_dict()) + self.assertEqual(cc.to_dict(), dict(schemes=["md5_crypt","bsdi_crypt","des_crypt"], + default="bsdi_crypt")) + + self.assertRaises(TypeError, setattr, cc, 'policy', None) + self.assertRaises(TypeError, CryptContext, policy='x') + + def test_01_replace(self): + "test replace()" + + cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) + self.assertIs(cc.policy.get_handler(), hash.md5_crypt) + + cc2 = cc.replace() + self.assertIsNot(cc2, cc) + # NOTE: was not able to maintain backward compatibility with this... + ##self.assertIs(cc2.policy, cc.policy) + + cc3 = cc.replace(default="bsdi_crypt") + self.assertIsNot(cc3, cc) + # NOTE: was not able to maintain backward compatibility with this... + ##self.assertIs(cc3.policy, cc.policy) + self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt) + + def test_02_no_handlers(self): + "test no handlers" + + # check constructor... + cc = CryptContext() + self.assertRaises(KeyError, cc.identify, 'hash', required=True) + self.assertRaises(KeyError, cc.encrypt, 'secret') + self.assertRaises(KeyError, cc.verify, 'secret', 'hash') + + # check updating policy after the fact... + cc = CryptContext(['md5_crypt']) + p = CryptPolicy(schemes=[]) + cc.policy = p + + self.assertRaises(KeyError, cc.identify, 'hash', required=True) + self.assertRaises(KeyError, cc.encrypt, 'secret') + self.assertRaises(KeyError, cc.verify, 'secret', 'hash') + + #=================================================================== + # policy adaptation + #=================================================================== + sample_policy_1 = dict( + schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt", + "sha256_crypt"], + deprecated = [ "des_crypt", ], + default = "sha256_crypt", + bsdi_crypt__max_rounds = 30, + bsdi_crypt__default_rounds = 25, + bsdi_crypt__vary_rounds = 0, + sha256_crypt__max_rounds = 3000, + sha256_crypt__min_rounds = 2000, + sha256_crypt__default_rounds = 3000, + phpass__ident = "H", + phpass__default_rounds = 7, + ) + + def test_12_hash_needs_update(self): + "test hash_needs_update() method" + cc = CryptContext(**self.sample_policy_1) + + # check deprecated scheme + self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA')) + self.assertFalse(cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) + + # check min rounds + self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) + self.assertFalse(cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) + + # check max rounds + self.assertFalse(cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) + self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) + + #=================================================================== + # border cases + #=================================================================== + def test_30_nonstring_hash(self): + "test non-string hash values cause error" + # + # test hash=None or some other non-string causes TypeError + # and that explicit-scheme code path behaves the same. + # + cc = CryptContext(["des_crypt"]) + for hash, kwds in [ + (None, {}), + (None, {"scheme": "des_crypt"}), + (1, {}), + ((), {}), + ]: + + self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds) + + cc2 = CryptContext(["mysql323"]) + self.assertRaises(TypeError, cc2.hash_needs_update, None) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# LazyCryptContext +#============================================================================= +class dummy_2(uh.StaticHandler): + name = "dummy_2" + +class LazyCryptContextTest(TestCase): + descriptionPrefix = "LazyCryptContext" + + def setUp(self): + TestCase.setUp(self) + + # make sure this isn't registered before OR after + unload_handler_name("dummy_2") + self.addCleanup(unload_handler_name, "dummy_2") + + # silence some warnings + warnings.filterwarnings("ignore", + r"CryptContext\(\)\.replace\(\) has been deprecated") + warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*") + + def test_kwd_constructor(self): + "test plain kwds" + self.assertFalse(has_crypt_handler("dummy_2")) + register_crypt_handler_path("dummy_2", "passlib.tests.test_context") + + cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + + self.assertFalse(has_crypt_handler("dummy_2", True)) + + self.assertTrue(cc.policy.handler_is_deprecated("des_crypt")) + self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"]) + + self.assertTrue(has_crypt_handler("dummy_2", True)) + + def test_callable_constructor(self): + "test create_policy() hook, returning CryptPolicy" + self.assertFalse(has_crypt_handler("dummy_2")) + register_crypt_handler_path("dummy_2", "passlib.tests.test_context") + + def create_policy(flag=False): + self.assertTrue(flag) + return CryptPolicy(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) + + cc = LazyCryptContext(create_policy=create_policy, flag=True) + + self.assertFalse(has_crypt_handler("dummy_2", True)) + + self.assertTrue(cc.policy.handler_is_deprecated("des_crypt")) + self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"]) + + self.assertTrue(has_crypt_handler("dummy_2", True)) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_drivers.py passlib-1.6.1/passlib/tests/test_drivers.py --- passlib-1.5.3/passlib/tests/test_drivers.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_drivers.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,1326 +0,0 @@ -"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" -#========================================================= -#imports -#========================================================= -from __future__ import with_statement -#core -import hashlib -import logging; log = logging.getLogger(__name__) -import warnings -#site -#pkg -from passlib import hash -from passlib.tests.utils import TestCase, HandlerCase, create_backend_case, \ - enable_option, b, catch_warnings -#module - - -#========================================================= -#some -#========================================================= - -#some common unicode passwords which used as test cases... -UPASS_WAV = u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2' -UPASS_USD = u"\u20AC\u00A5$" -UPASS_TABLE = u"t\u00e1\u0411\u2113\u0259" - -#========================================================= -#apr md5 crypt -#========================================================= -from passlib.handlers.md5_crypt import apr_md5_crypt -class AprMd5CryptTest(HandlerCase): - handler = apr_md5_crypt - - #values taken from http://httpd.apache.org/docs/2.2/misc/password_encryptions.html - known_correct_hashes = [ - ('myPassword', '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA/'), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA!' - ] - -#========================================================= -#bcrypt -#========================================================= -class _BCryptTest(HandlerCase): - "base for BCrypt test cases" - - handler = hash.bcrypt - secret_chars = 72 - - known_correct_hashes = [ - #selected bcrypt test vectors - ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), - ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), - ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), - ('abcdefghijklmnopqrstuvwxyz', - '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), - ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', - '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), - ] - - known_unidentified_hashes = [ - #unsupported minor version - "$2b$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", - - #rounds not zero-padded (pybcrypt rejects this, therefore so do we) - '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.' - - #NOTE: salts with padding bits set are technically malformed, - # but that's one we can reliably correct & issue warning for. - ] - - #=============================================================== - # extra tests - #=============================================================== - def iter_external_verifiers(self): - try: - from bcrypt import hashpw - except ImportError: - pass - else: - def check_pybcrypt(secret, hash): - self.assertEqual(hashpw(secret, hash), hash, - "pybcrypt: bcrypt.hashpw(%r,%r):" % (secret, hash)) - yield check_pybcrypt - - try: - from bcryptor.engine import Engine - except ImportError: - pass - else: - def check_bcryptor(secret, hash): - result = Engine(False).hash_key(secret, hash) - self.assertEqual(result, hash, - "bcryptor: hash_key(%r,%r):" % (secret, hash)) - yield check_bcryptor - - def test_90_idents(self): - "test identifier validation" - handler = self.handler - - kwds = dict(checksum='8CIhhFCj15KqqFvo/n.Jatx8dJ92f82', - salt='VlsfIX9.apXuQBr6tego0.', - rounds=12, ident="2a", strict=True) - - handler(**kwds) - - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) - - del kwds['strict'] - - kwds['ident'] = 'Q' - self.assertRaises(ValueError, handler, **kwds) - - #=============================================================== - # see issue 25 - https://code.google.com/p/passlib/issues/detail?id=25 - # bcrypt's salt ends with 4 padding bits. - # openbsd, pybcrypt, etc assume these bits are always 0. - # passlib <= 1.5.2 generated salts where this wasn't usually the case. - # as of 1.5.3, we want to always generate salts w/ 0 padding, - # and clear the padding of any incoming hashes - #=============================================================== - def do_genconfig(self, **kwds): - # correct provided salts to handle ending correctly, - # so test_33_genconfig_saltchars doesn't throw warnings. - if 'salt' in kwds: - from passlib.handlers.bcrypt import BCHARS, BSLAST - salt = kwds['salt'] - if salt and salt[-1] not in BSLAST: - salt = salt[:-1] + BCHARS[BCHARS.index(salt[-1])&~15] - kwds['salt'] = salt - return self.handler.genconfig(**kwds) - - def test_91_bcrypt_padding(self): - "test passlib correctly handles bcrypt padding bits" - bcrypt = self.handler - - def check_warning(wlog): - self.assertWarningMatches(wlog.pop(0), - message_re="^encountered a bcrypt hash with incorrectly set padding bits.*", - ) - self.assertFalse(wlog) - - def check_padding(hash): - "check bcrypt hash doesn't have salt padding bits set" - assert hash.startswith("$2a$") and len(hash) >= 28 - self.assertTrue(hash[28] in BSLAST, - "padding bits set in hash: %r" % (hash,)) - - #=============================================================== - # test generated salts - #=============================================================== - from passlib.handlers.bcrypt import BCHARS, BSLAST - - # make sure genconfig & encrypt don't return bad hashes. - # bug had 15/16 chance of occurring every time salt generated. - # so we call it a few different way a number of times. - for i in xrange(6): - check_padding(bcrypt.genconfig()) - for i in xrange(3): - check_padding(bcrypt.encrypt("bob", rounds=bcrypt.min_rounds)) - - # check passing salt to genconfig causes it to be normalized. - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") - - hash = bcrypt.genconfig(salt="."*21 + "A.") - check_warning(wlog) - self.assertEqual(hash, "$2a$12$" + "." * 22) - - hash = bcrypt.genconfig(salt="."*23) - self.assertFalse(wlog) - self.assertEqual(hash, "$2a$12$" + "." * 22) - - #=============================================================== - # test handling existing hashes - #=============================================================== - - # 2 bits of salt padding set - PASS1 = "loppux" - BAD1 = "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C" - GOOD1 = "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C" - - # all 4 bits of salt padding set - PASS2 = "Passlib11" - BAD2 = "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK" - GOOD2 = "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK" - - # bad checksum padding - PASS3 = "test" - BAD3 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV" - GOOD3 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" - - # make sure genhash() corrects input - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") - - self.assertEqual(bcrypt.genhash(PASS1, BAD1), GOOD1) - check_warning(wlog) - - self.assertEqual(bcrypt.genhash(PASS2, BAD2), GOOD2) - check_warning(wlog) - - self.assertEqual(bcrypt.genhash(PASS2, GOOD2), GOOD2) - self.assertFalse(wlog) - - self.assertEqual(bcrypt.genhash(PASS3, BAD3), GOOD3) - check_warning(wlog) - self.assertFalse(wlog) - - # make sure verify works on both bad and good hashes - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") - - self.assertTrue(bcrypt.verify(PASS1, BAD1)) - check_warning(wlog) - - self.assertTrue(bcrypt.verify(PASS1, GOOD1)) - self.assertFalse(wlog) - - #=============================================================== - # test normhash cleans things up correctly - #=============================================================== - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") - self.assertEqual(bcrypt.normhash(BAD1), GOOD1) - self.assertEqual(bcrypt.normhash(BAD2), GOOD2) - self.assertEqual(bcrypt.normhash(GOOD1), GOOD1) - self.assertEqual(bcrypt.normhash(GOOD2), GOOD2) - self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc") - -hash.bcrypt._no_backends_msg() #call this for coverage purposes - -#create test cases for specific backends -Pybcrypt_BCryptTest = create_backend_case(_BCryptTest, "pybcrypt") -Bcryptor_BCryptTest = create_backend_case(_BCryptTest, "bcryptor") -OsCrypt_BCryptTest = create_backend_case(_BCryptTest, "os_crypt") - -#========================================================= -#bigcrypt -#========================================================= -from passlib.handlers.des_crypt import bigcrypt - -class BigCryptTest(HandlerCase): - handler = bigcrypt - - #TODO: find an authortative source of test vectors, - #these were found in docs and messages on the web. - known_correct_hashes = [ - ("passphrase", "qiyh4XPJGsOZ2MEAyLkfWqeQ"), - ("This is very long passwd", "f8.SVpL2fvwjkAnxn8/rgTkwvrif6bjYB5c"), - ] - - known_unidentified_hashes = [ - #one char short - "qiyh4XPJGsOZ2MEAyLkfWqe" - ] - - #omit des_crypt from known other, it looks like bigcrypt - known_other_hashes = [row for row in HandlerCase.known_other_hashes if row[0] != "des_crypt"] - -#========================================================= -#bsdi crypt -#========================================================= -class _BSDiCryptTest(HandlerCase): - "test BSDiCrypt algorithm" - handler = hash.bsdi_crypt - known_correct_hashes = [ - (" ", "_K1..crsmZxOLzfJH8iw"), - ("my", '_KR/.crsmykRplHbAvwA'), #<- to detect old 12-bit rounds bug - ("my socra", "_K1..crsmf/9NzZr1fLM"), - ("my socrates", '_K1..crsmOv1rbde9A9o'), - ("my socrates note", "_K1..crsm/2qeAhdISMA"), - ] - known_unidentified_hashes = [ - #bad char in otherwise correctly formatted hash - "_K1.!crsmZxOLzfJH8iw" - ] - -OsCrypt_BSDiCryptTest = create_backend_case(_BSDiCryptTest, "os_crypt") -Builtin_BSDiCryptTest = create_backend_case(_BSDiCryptTest, "builtin") - -#========================================================= -#crypt16 -#========================================================= -from passlib.handlers.des_crypt import crypt16 - -class Crypt16Test(HandlerCase): - handler = crypt16 - secret_chars = 16 - - #TODO: find an authortative source of test vectors - #instead of just msgs around the web - # (eg http://seclists.org/bugtraq/1999/Mar/76) - known_correct_hashes = [ - ("passphrase", "qi8H8R7OM4xMUNMPuRAZxlY."), - ("printf", "aaCjFz4Sh8Eg2QSqAReePlq6"), - ("printf", "AA/xje2RyeiSU0iBY3PDwjYo"), - ("LOLOAQICI82QB4IP", "/.FcK3mad6JwYt8LVmDqz9Lc"), - ("LOLOAQICI", "/.FcK3mad6JwYSaRHJoTPzY2"), - ("LOLOAQIC", "/.FcK3mad6JwYelhbtlysKy6"), - ("L", "/.CIu/PzYCkl6elhbtlysKy6"), - ] -#========================================================= -#des crypt -#========================================================= -from passlib.handlers.des_crypt import des_crypt - -class _DesCryptTest(HandlerCase): - "test des-crypt algorithm" - handler = des_crypt - secret_chars = 8 - - known_correct_hashes = [ - #secret, example hash which matches secret - ('', 'OgAwTx2l6NADI'), - (' ', '/Hk.VPuwQTXbc'), - ('test', 'N1tQbOFcM5fpg'), - ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'), - ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'), - ('AlOtBsOl', 'cEpWz5IUCShqM'), - (u'hell\u00D6', 'saykDgk3BPZ9E'), - ] - known_unidentified_hashes = [ - #bad char in otherwise correctly formatted hash - '!gAwTx2l6NADI', - ] - - def test_invalid_secret_chars(self): - self.assertRaises(ValueError, self.do_encrypt, 'sec\x00t') - -OsCrypt_DesCryptTest = create_backend_case(_DesCryptTest, "os_crypt") -Builtin_DesCryptTest = create_backend_case(_DesCryptTest, "builtin") - -#========================================================= -#django -#========================================================= -class _DjangoHelper(object): - - def test_django_reference(self): - "run known correct hashes through Django's check_password()" - if not self.known_correct_hashes: - return self.skipTest("no known correct hashes specified") - from passlib.tests.test_ext_django import has_django1 - if not has_django1: - return self.skipTest("Django not installed") - from django.contrib.auth.models import check_password - for secret, hash in self.all_correct_hashes: - self.assertTrue(check_password(secret, hash)) - self.assertFalse(check_password('x' + secret, hash)) - -class DjangoDisabledTest(HandlerCase): - "test django_disabled" - - #NOTE: this class behaves VERY differently from a normal password hash, - #so we subclass & disable a number of the default tests. - #TODO: combine these features w/ unix_fallback and other disabled handlers. - - handler = hash.django_disabled - handler_type = "disabled" - - def test_20_verify_positive(self): - for secret, result in [ - ("password", "!"), - ("", "!"), - ]: - self.assertFalse(self.do_verify(secret, result)) - - def test_50_encrypt_plain(self): - "test encrypt() basic behavior" - secret = UPASS_USD - result = self.do_encrypt(secret) - self.assertEqual(result, "!") - self.assertTrue(not self.do_verify(secret, result)) - -class DjangoDesCryptTest(HandlerCase, _DjangoHelper): - "test django_des_crypt" - handler = hash.django_des_crypt - secret_chars = 8 - - known_correct_hashes = [ - #ensures only first two digits of salt count. - ("password", 'crypt$c2$c2M87q...WWcU'), - ("password", 'crypt$c2e86$c2M87q...WWcU'), - ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'), - - #ensures utf-8 used for unicode - (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'), - (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'), - (u"hell\u00D6", "crypt$sa$saykDgk3BPZ9E"), - - #prevent regression of issue 22 - ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), - ] - - known_unidentified_hashes = [ - 'sha1$aa$bb', - ] - - known_malformed_hashes = [ - # checksum too short - 'crypt$c2$c2M87q', - - # salt must be >2 - 'crypt$$c2M87q...WWcU', - 'crypt$f$c2M87q...WWcU', - - # this format duplicates salt inside checksum, - # reject any where the two copies don't match - 'crypt$ffe86$c2M87q...WWcU', - ] - -class DjangoSaltedMd5Test(HandlerCase, _DjangoHelper): - "test django_salted_md5" - handler = hash.django_salted_md5 - - known_correct_hashes = [ - #test extra large salt - ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), - - #test unicode uses utf-8 - (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), - (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), - ] - - known_unidentified_hashes = [ - 'sha1$aa$bb', - ] - - known_malformed_hashes = [ - # checksum too short - 'md5$aa$bb', - ] - -class DjangoSaltedSha1Test(HandlerCase, _DjangoHelper): - "test django_salted_sha1" - handler = hash.django_salted_sha1 - - known_correct_hashes = [ - #test extra large salt - ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), - - #test unicode uses utf-8 - (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), - (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), - - #generic password - ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), - ] - - known_unidentified_hashes = [ - 'md5$aa$bb', - ] - - known_malformed_hashes = [ - # checksum too short - 'sha1$c2e86$0f75', - ] - -#========================================================= -#fshp -#========================================================= -class FSHPTest(HandlerCase): - "test fshp algorithm" - handler = hash.fshp - - known_correct_hashes = [ - #secret, example hash which matches secret - - #test vectors from FSHP reference implementation - ('test', '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M='), - - ('test', - '{FSHP1|8|4096}MTIzNDU2NzjTdHcmoXwNc0f' - 'f9+ArUHoN0CvlbPZpxFi1C6RDM/MHSA==' - ), - - ('OrpheanBeholderScryDoubt', - '{FSHP1|8|4096}GVSUFDAjdh0vBosn1GUhz' - 'GLHP7BmkbCZVH/3TQqGIjADXpc+6NCg3g==' - ), - ('ExecuteOrder66', - '{FSHP3|16|8192}0aY7rZQ+/PR+Rd5/I9ss' - 'RM7cjguyT8ibypNaSp/U1uziNO3BVlg5qPU' - 'ng+zHUDQC3ao/JbzOnIBUtAeWHEy7a2vZeZ' - '7jAwyJJa2EqOsq4Io=' - ), - ] - - known_unidentified_hashes = [ - #bad char in otherwise correctly formatted hash - '{FSHX0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', - 'FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', - ] - - known_malformed_hashes = [ - #wrong salt size - '{FSHP0|1|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', - - #bad rounds - '{FSHP0|0|A}qUqP5cyxm6YcTAhz05Hph5gvu9M=', - ] - -#========================================================= -#hex digests -#========================================================= -from passlib.handlers import digests - -class HexMd4Test(HandlerCase): - handler = digests.hex_md4 - known_correct_hashes = [ ("password", '8a9d093f14f8701df17732b2bb182c74')] - -class HexMd5Test(HandlerCase): - handler = digests.hex_md5 - known_correct_hashes = [ ("password", '5f4dcc3b5aa765d61d8327deb882cf99')] - -class HexSha1Test(HandlerCase): - handler = digests.hex_sha1 - known_correct_hashes = [ ("password", '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8')] - -class HexSha256Test(HandlerCase): - handler = digests.hex_sha256 - known_correct_hashes = [ ("password", '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8')] - -class HexSha512Test(HandlerCase): - handler = digests.hex_sha512 - known_correct_hashes = [ ("password", 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86')] - -#========================================================= -#ldap hashes -#========================================================= -from passlib.handlers import ldap_digests - -class LdapMd5Test(HandlerCase): - handler = ldap_digests.ldap_md5 - known_correct_hashes = [ ("helloworld", '{MD5}/F4DjTilcDIIVEHn/nAQsA==')] - -class LdapSha1Test(HandlerCase): - handler = ldap_digests.ldap_sha1 - known_correct_hashes = [ ("helloworld", '{SHA}at+xg6SiyUovktq1redipHiJpaE=')] - -class LdapSaltedMd5Test(HandlerCase): - handler = ldap_digests.ldap_salted_md5 - known_correct_hashes = [ ("testing1234", '{SMD5}UjFY34os/pnZQ3oQOzjqGu4yeXE=')] - -class LdapSaltedSha1Test(HandlerCase): - handler = ldap_digests.ldap_salted_sha1 - known_correct_hashes = [ ("testing123", '{SSHA}0c0blFTXXNuAMHECS4uxrj3ZieMoWImr'), - ("secret", "{SSHA}0H+zTv8o4MR4H43n03eCsvw1luG8LdB7"), - ] - -class LdapPlaintextTest(HandlerCase): - handler = ldap_digests.ldap_plaintext - known_correct_hashes = [ ("password", 'password') ] - known_unidentified_hashes = [ "{FOO}bar" ] - - known_other_hashes = [ ("ldap_md5", "{MD5}/F4DjTilcDIIVEHn/nAQsA==")] - -#NOTE: since the ldap_{crypt} handlers are all wrappers, -# don't need separate test. have just one for end-to-end testing purposes. - -class _LdapMd5CryptTest(HandlerCase): - handler = ldap_digests.ldap_md5_crypt - - known_correct_hashes = [ - ('', '{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), - (' ', '{CRYPT}$1$m/5ee7ol$bZn0kIBFipq39e.KDXX8I0'), - ('test', '{CRYPT}$1$ec6XvcoW$ghEtNK2U1MC5l.Dwgi3020'), - ('Compl3X AlphaNu3meric', '{CRYPT}$1$nX1e7EeI$ljQn72ZUgt6Wxd9hfvHdV0'), - ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '{CRYPT}$1$jQS7o98J$V6iTcr71CGgwW2laf17pi1'), - ('test', '{CRYPT}$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', - ] - -OsCrypt_LdapMd5CryptTest = create_backend_case(_LdapMd5CryptTest, "os_crypt") -Builtin_LdapMd5CryptTest = create_backend_case(_LdapMd5CryptTest, "builtin") - -#========================================================= -#ldap_pbkdf2_{digest} -#========================================================= -from passlib.handlers import pbkdf2 as pk2 - -#NOTE: since these are all wrappers for the pbkdf2_{digest} hasehs, -# they don't extensive separate testing. - -class LdapPbkdf2Test(TestCase): - - def test_wrappers(self): - "test ldap pbkdf2 wrappers" - - self.assertTrue( - pk2.ldap_pbkdf2_sha1.verify( - "password", - '{PBKDF2}1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI', - ) - ) - - self.assertTrue( - pk2.ldap_pbkdf2_sha256.verify( - "password", - '{PBKDF2-SHA256}1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg' - '.fJPeq1h/gXXY7acBp9/6c.tmQ' - ) - ) - - self.assertTrue( - pk2.ldap_pbkdf2_sha512.verify( - "password", - '{PBKDF2-SHA512}1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' - '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' - ) - ) - -#========================================================= -#md5 crypt -#========================================================= -from passlib.handlers.md5_crypt import md5_crypt, raw_md5_crypt -class _Md5CryptTest(HandlerCase): - handler = md5_crypt - - known_correct_hashes = [ - #NOTE: would need to patch HandlerCase to coerce hashes - #to_hash_str() for this first one to work under py3. -## ('', b('$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.')), - ('', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), - (' ', '$1$m/5ee7ol$bZn0kIBFipq39e.KDXX8I0'), - ('test', '$1$ec6XvcoW$ghEtNK2U1MC5l.Dwgi3020'), - ('Compl3X AlphaNu3meric', '$1$nX1e7EeI$ljQn72ZUgt6Wxd9hfvHdV0'), - ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$1$jQS7o98J$V6iTcr71CGgwW2laf17pi1'), - ('test', '$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), - (b('test'), '$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', - ] - - def test_raw(self): - self.assertEqual(raw_md5_crypt(u's',u's'*16), u'YgmLTApYTv12qgTwBoj8i/') - -OsCrypt_Md5CryptTest = create_backend_case(_Md5CryptTest, "os_crypt") -Builtin_Md5CryptTest = create_backend_case(_Md5CryptTest, "builtin") - -#========================================================= -#mysql 323 & 41 -#========================================================= -from passlib.handlers.mysql import mysql323, mysql41 - -class Mysql323Test(HandlerCase): - handler = mysql323 - - known_correct_hashes = [ - ('mypass', '6f8c114b58f2ce9e'), - ] - known_unidentified_hashes = [ - #bad char in otherwise correct hash - '6z8c114b58f2ce9e', - ] - - def test_whitespace(self): - "check whitespace is ignored per spec" - h = self.do_encrypt("mypass") - h2 = self.do_encrypt("my pass") - self.assertEqual(h, h2) - -class Mysql41Test(HandlerCase): - handler = mysql41 - known_correct_hashes = [ - ('mypass', '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'), - ] - known_unidentified_hashes = [ - #bad char in otherwise correct hash - '*6Z8989366EAF75BB670AD8EA7A7FC1176A95CEF4', - ] - -#========================================================= -#NTHASH for unix -#========================================================= -from passlib.handlers.nthash import nthash - -class NTHashTest(HandlerCase): - handler = nthash - - known_correct_hashes = [ - ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), - ('passphrase', '$NT$7f8fe03093cc84b267b109625f6bbf4b'), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '$3$$7f8fe03093cc84b267b109625f6bbfxb', - ] - - def test_idents(self): - handler = self.handler - - kwds = dict(checksum='7f8fe03093cc84b267b109625f6bbf4b', ident="3", strict=True) - handler(**kwds) - - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) - - del kwds['strict'] - kwds['ident'] = 'Q' - self.assertRaises(ValueError, handler, **kwds) - -#========================================================= -#oracle 10 & 11 -#========================================================= -from passlib.handlers.oracle import oracle10, oracle11 - -class Oracle10Test(HandlerCase): - handler = oracle10 - - known_correct_hashes = [ - # ((secret,user),hash) - (('tiger', 'scott'), 'F894844C34402B67'), - ((u'ttTiGGeR', u'ScO'), '7AA1A84E31ED7771'), - (("d_syspw", "SYSTEM"), '1B9F1F9A5CB9EB31'), - (("strat_passwd", "strat_user"), 'AEBEDBB4EFB5225B'), - #TODO: get more test vectors (especially ones which properly test unicode / non-ascii) - #existing vectors taken from - http://www.petefinnigan.com/default/default_password_list.htm - ] - - known_unidentified_hashes = [ - #bad 'z' char in otherwise correct hash - 'F894844C34402B6Z', - ] - - def test_user(self): - "check user kwd is required for encrypt/verify" - self.assertRaises(TypeError, self.handler.encrypt, 'mypass') - self.assertRaises(ValueError, self.handler.encrypt, 'mypass', None) - self.assertRaises(TypeError, self.handler.verify, 'mypass', 'CC60FA650C497E52') - - #NOTE: all of the methods below are merely to override - # the default test harness in order to insert a default username - # when encrypt/verify/etc are called. - - def create_mismatch(self, secret): - if isinstance(secret, tuple): - secret, user = secret - return 'x' + secret, user - else: - return 'x' + secret - - def do_encrypt(self, secret, **kwds): - if isinstance(secret, tuple): - secret, user = secret - else: - user = 'default' - assert 'user' not in kwds - kwds['user'] = user - return self.handler.encrypt(secret, **kwds) - - def do_verify(self, secret, hash): - if isinstance(secret, tuple): - secret, user = secret - else: - user = 'default' - return self.handler.verify(secret, hash, user=user) - - def do_genhash(self, secret, config): - if isinstance(secret, tuple): - secret, user = secret - else: - user = 'default' - return self.handler.genhash(secret, config, user=user) - -class Oracle11Test(HandlerCase): - handler = oracle11 - known_correct_hashes = [ - ("SHAlala", "S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C"), - #TODO: find more test vectors - ] - -#========================================================= -#pbkdf2 hashes -#========================================================= -from passlib.handlers import pbkdf2 as pk2 - -class AtlassianPbkdf2Sha1Test(HandlerCase): - handler = pk2.atlassian_pbkdf2_sha1 - known_correct_hashes = [ - ("admin", '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/p'), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - "{PKCS5S2}cE9Yq6Am5tQGdHSHhky2XLeOnURwzaLBG2sur7FHKpvy2u0qDn6GcVGRjlmJoIUy"), - ] - - known_malformed_hashes = [ - #bad char - '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq!/p' - - #bad size, missing padding - '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/' - - #bad size, with correct padding - '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/=' - ] - -class Pbkdf2Sha1Test(HandlerCase): - handler = pk2.pbkdf2_sha1 - known_correct_hashes = [ - ("password", '$pbkdf2$1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI'), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - '$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc'), - ] - -class Pbkdf2Sha256Test(HandlerCase): - handler = pk2.pbkdf2_sha256 - known_correct_hashes = [ - ("password", - '$pbkdf2-sha256$1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg.fJPeq1h/gXXY7acBp9/6c.tmQ' - ), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - '$pbkdf2-sha256$1212$3SABFJGDtyhrQMVt1uABPw$WyaUoqCLgvz97s523nF4iuOqZNbp5Nt8do/cuaa7AiI' - ), - ] - -class Pbkdf2Sha512Test(HandlerCase): - handler = pk2.pbkdf2_sha512 - known_correct_hashes = [ - ("password", - '$pbkdf2-sha512$1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' - '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' - ), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - '$pbkdf2-sha512$1212$KkbvoKGsAIcF8IslDR6skQ$8be/PRmd88Ps8fmPowCJt' - 'tH9G3vgxpG.Krjt3KT.NP6cKJ0V4Prarqf.HBwz0dCkJ6xgWnSj2ynXSV7MlvMa8Q' - ), - ] - -class CtaPbkdf2Sha1Test(HandlerCase): - handler = pk2.cta_pbkdf2_sha1 - known_correct_hashes = [ - #test vectors from original implementation - (u"hashy the \N{SNOWMAN}", '$p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0='), - - #additional test vectors - ("password", "$p5k2$1$$h1TDLGSw9ST8UMAPeIE13i0t12c="), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - "$p5k2$4321$OTg3NjU0MzIx$jINJrSvZ3LXeIbUdrJkRpN62_WQ="), - ] - -class DlitzPbkdf2Sha1Test(HandlerCase): - handler = pk2.dlitz_pbkdf2_sha1 - known_correct_hashes = [ - #test vectors from original implementation - ('cloadm', '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'), - ('gnu', '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'), - ('dcl', '$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL'), - ('spam', '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'), - ] - -class GrubPbkdf2Sha512Test(HandlerCase): - handler = pk2.grub_pbkdf2_sha512 - known_correct_hashes = [ - #test vectors generated from cmd line tool - - #salt=32 bytes - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - 'grub.pbkdf2.sha512.10000.BCAC1CEC5E4341C8C511C529' - '7FA877BE91C2817B32A35A3ECF5CA6B8B257F751.6968526A' - '2A5B1AEEE0A29A9E057336B48D388FFB3F600233237223C21' - '04DE1752CEC35B0DD1ED49563398A282C0F471099C2803FBA' - '47C7919CABC43192C68F60'), - - #salt=64 bytes - ('toomanysecrets', - 'grub.pbkdf2.sha512.10000.9B436BB6978682363D5C449B' - 'BEAB322676946C632208BC1294D51F47174A9A3B04A7E4785' - '986CD4EA7470FAB8FE9F6BD522D1FC6C51109A8596FB7AD48' - '7C4493.0FE5EF169AFFCB67D86E2581B1E251D88C777B98BA' - '2D3256ECC9F765D84956FC5CA5C4B6FD711AA285F0A04DCF4' - '634083F9A20F4B6F339A52FBD6BED618E527B'), - - ] - -#========================================================= -#PHPass Portable Crypt -#========================================================= -from passlib.handlers.phpass import phpass - -class PHPassTest(HandlerCase): - handler = phpass - - known_correct_hashes = [ - ('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'), - ('compL3X!', '$P$FiS0N5L672xzQx1rt1vgdJQRYKnQM9/'), - ('test12345', '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0'), #from the source - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r!L0', - ] - - def test_idents(self): - handler = self.handler - - kwds = dict(checksum='eRo7ud9Fh4E2PdI0S3r.L0', salt='IQRaTwmf', rounds=9, ident="P", strict=True) - handler(**kwds) - - kwds['ident'] = None - self.assertRaises(ValueError, handler, **kwds) - - del kwds['strict'] - kwds['ident'] = 'Q' - self.assertRaises(ValueError, handler, **kwds) - -#========================================================= -#plaintext -#========================================================= -from passlib.handlers.misc import plaintext - -class PlaintextTest(HandlerCase): - handler = plaintext - - known_correct_hashes = [ - ('',''), - ('password', 'password'), - ] - - known_other_hashes = [] #all strings are identified as belonging to this scheme - - accepts_empty_hash = True - -#========================================================= -#postgres_md5 -#========================================================= -from passlib.handlers.postgres import postgres_md5 - -class PostgresMD5CryptTest(HandlerCase): - handler = postgres_md5 - known_correct_hashes = [ - # ((secret,user),hash) - (('mypass', 'postgres'), 'md55fba2ea04fd36069d2574ea71c8efe9d'), - (('mypass', 'root'), 'md540c31989b20437833f697e485811254b'), - (("testpassword",'testuser'), 'md5d4fc5129cc2c25465a5370113ae9835f'), - ] - known_unidentified_hashes = [ - #bad 'z' char in otherwise correct hash - 'md54zc31989b20437833f697e485811254b', - ] - - #NOTE: used to support secret=(password, user) format, but removed it for now. - ##def test_tuple_mode(self): - ## "check tuple mode works for encrypt/verify" - ## self.assertEqual(self.handler.encrypt(('mypass', 'postgres')), - ## 'md55fba2ea04fd36069d2574ea71c8efe9d') - ## self.assertEqual(self.handler.verify(('mypass', 'postgres'), - ## 'md55fba2ea04fd36069d2574ea71c8efe9d'), True) - - def test_user(self): - "check user kwd is required for encrypt/verify" - self.handler.encrypt("mypass", u'user') - self.assertRaises(TypeError, self.handler.encrypt, 'mypass') - self.assertRaises(ValueError, self.handler.encrypt, 'mypass', None) - self.assertRaises(TypeError, self.handler.verify, 'mypass', 'md55fba2ea04fd36069d2574ea71c8efe9d') - - def create_mismatch(self, secret): - if isinstance(secret, tuple): - secret, user = secret - return 'x' + secret, user - else: - return 'x' + secret - - def do_encrypt(self, secret, **kwds): - if isinstance(secret, tuple): - secret, user = secret - else: - user = 'default' - assert 'user' not in kwds - kwds['user'] = user - return self.handler.encrypt(secret, **kwds) - - def do_verify(self, secret, hash): - if isinstance(secret, tuple): - secret, user = secret - else: - user = 'default' - return self.handler.verify(secret, hash, user=user) - - def do_genhash(self, secret, config): - if isinstance(secret, tuple): - secret, user = secret - else: - user = 'default' - return self.handler.genhash(secret, config, user=user) - -#========================================================= -# (netbsd's) sha1 crypt -#========================================================= -class _SHA1CryptTest(HandlerCase): - handler = hash.sha1_crypt - - known_correct_hashes = [ - ("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"), - ("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '$sha1$21773$u!7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', - - #zero padded rounds - '$sha1$01773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', - ] - -OsCrypt_SHA1CryptTest = create_backend_case(_SHA1CryptTest, "os_crypt") -Builtin_SHA1CryptTest = create_backend_case(_SHA1CryptTest, "builtin") - -#========================================================= -#roundup -#========================================================= - -#NOTE: all roundup hashes use PrefixWrapper, -# so there's nothing natively to test. -# so we just have a few quick cases... -from passlib.handlers import roundup - -class RoundupTest(TestCase): - - def _test_pair(self, h, secret, hash): - self.assertTrue(h.verify(secret, hash)) - self.assertFalse(h.verify('x'+secret, hash)) - - def test_pairs(self): - self._test_pair( - roundup.ldap_hex_sha1, - "sekrit", - '{SHA}8d42e738c7adee551324955458b5e2c0b49ee655') - - self._test_pair( - roundup.ldap_hex_md5, - "sekrit", - '{MD5}ccbc53f4464604e714f69dd11138d8b5') - - self._test_pair( - ldap_digests.ldap_des_crypt, - "sekrit", - '{CRYPT}nFia0rj2TT59A') - - self._test_pair( - roundup.roundup_plaintext, - "sekrit", - '{plaintext}sekrit') - - self._test_pair( - pk2.ldap_pbkdf2_sha1, - "sekrit", - '{PBKDF2}5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE') - -#========================================================= -#sha256-crypt -#========================================================= -from passlib.handlers.sha2_crypt import sha256_crypt, raw_sha_crypt - -class _SHA256CryptTest(HandlerCase): - handler = sha256_crypt - - known_correct_hashes = [ - ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'), - (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'), - ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'), - ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'), - ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'), - (u'with unic\u00D6de', '$5$rounds=1000$IbG0EuGQXw5EkMdP$LQ5AfPf13KufFsKtmazqnzSGZ4pxtUNw3woQ.ELRDF4'), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - '$5$rounds=10428$uy/:jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMeZGsGx2aBvxTvDFI613c3', - - #zero-padded rounds - '$5$rounds=010428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3', - ] - - #NOTE: these test cases taken from official specification at http://www.akkadia.org/drepper/SHA-crypt.txt - known_correct_configs = [ - #config, secret, result - ( "$5$saltstring", "Hello world!", - "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5" ), - ( "$5$rounds=10000$saltstringsaltstring", "Hello world!", - "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2." - "opqey6IcA" ), - ( "$5$rounds=5000$toolongsaltstring", "This is just a test", - "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8" - "mGRcvxa5" ), - ( "$5$rounds=1400$anotherlongsaltstring", - "a very much longer text to encrypt. This one even stretches over more" - "than one line.", - "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12" - "oP84Bnq1" ), - ( "$5$rounds=77777$short", - "we have a short salt string but not a short password", - "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/" ), - ( "$5$rounds=123456$asaltof16chars..", "a short string", - "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/" - "cZKmF/wJvD" ), - ( "$5$rounds=10$roundstoolow", "the minimum number is still observed", - "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL97" - "2bIC" ), - ] - - def filter_known_config_warnings(self): - warnings.filterwarnings("ignore", "sha256_crypt does not allow less than 1000 rounds: 10", UserWarning) - - def test_raw(self): - #run some tests on raw backend func to ensure it works right - self.assertEqual( - raw_sha_crypt(b('secret'), b('salt')*10, 1, hashlib.md5), - (b('\x1f\x96\x1cO\x11\xa9h\x12\xc4\xf3\x9c\xee\xf5\x93\xf3\xdd'), - b('saltsaltsaltsalt'), - 1000) - ) - self.assertRaises(ValueError, raw_sha_crypt, b('secret'), b('$'), 1, hashlib.md5) - -OsCrypt_SHA256CryptTest = create_backend_case(_SHA256CryptTest, "os_crypt") -Builtin_SHA256CryptTest = create_backend_case(_SHA256CryptTest, "builtin") - -#========================================================= -#test sha512-crypt -#========================================================= -from passlib.handlers.sha2_crypt import sha512_crypt - -class _SHA512CryptTest(HandlerCase): - handler = sha512_crypt - - known_correct_hashes = [ - ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'), - (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'), - ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'), - ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'), - ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'), - ] - - known_malformed_hashes = [ - #zero-padded rounds - '$6$rounds=011021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', - #bad char in otherwise correct hash - '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', - ] - - #NOTE: these test cases taken from official specification at http://www.akkadia.org/drepper/SHA-crypt.txt - known_correct_configs = [ - #config, secret, result - ("$6$saltstring", "Hello world!", - "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu" - "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ), - - ( "$6$rounds=10000$saltstringsaltstring", "Hello world!", - "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb" - "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ), - - ( "$6$rounds=5000$toolongsaltstring", "This is just a test", - "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ" - "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ), - - ( "$6$rounds=1400$anotherlongsaltstring", - "a very much longer text to encrypt. This one even stretches over more" - "than one line.", - "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP" - "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ), - - ( "$6$rounds=77777$short", - "we have a short salt string but not a short password", - "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g" - "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ), - - ( "$6$rounds=123456$asaltof16chars..", "a short string", - "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc" - "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ), - - ( "$6$rounds=10$roundstoolow", "the minimum number is still observed", - "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x" - "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ), - ] - - def filter_known_config_warnings(self): - warnings.filterwarnings("ignore", "sha512_crypt does not allow less than 1000 rounds: 10", UserWarning) - -OsCrypt_SHA512CryptTest = create_backend_case(_SHA512CryptTest, "os_crypt") -Builtin_SHA512CryptTest = create_backend_case(_SHA512CryptTest, "builtin") - -#========================================================= -#sun md5 crypt -#========================================================= -from passlib.handlers.sun_md5_crypt import sun_md5_crypt, raw_sun_md5_crypt - -class SunMD5CryptTest(HandlerCase): - handler = sun_md5_crypt - - known_correct_hashes = [ - #TODO: this scheme needs some real test vectors, - # especially due to the "bare salt" issue. - - #-------------------------------------- - #sample hashes culled from web messages - #-------------------------------------- - - #http://forums.halcyoninc.com/showthread.php?t=258 - ("Gpcs3_adm", "$md5$zrdhpMlZ$$wBvMOEqbSjU.hu5T2VEP01"), - - #http://www.c0t0d0s0.org/archives/4453-Less-known-Solaris-features-On-passwords-Part-2-Using-stronger-password-hashing.html - ("aa12345678", "$md5$vyy8.OVF$$FY4TWzuauRl4.VQNobqMY."), - - #http://www.cuddletech.com/blog/pivot/entry.php?id=778 - ("this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), - - #http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 - ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), - - #------------------------------- - #potential sample hashes - all have issues - #------------------------------- - - #source: http://solaris-training.com/301_HTML/docs/deepdiv.pdf page 27 - #FIXME: password unknown - #"$md5,rounds=8000$kS9FT1JC$$mnUrRO618lLah5iazwJ9m1" - - #source: http://www.visualexams.com/310-303.htm - #XXX: this has 9 salt chars unlike all other hashes. is that valid? - #FIXME: password unknown - #"$md5,rounds=2006$2amXesSj5$$kCF48vfPsHDjlKNXeEw7V." - - ] - - known_correct_configs = [ - #(config, secret, hash) - - #--------------------------- - #test salt string handling - # - #these tests attempt to verify that passlib is handling - #the "bare salt" issue (see sun md5 crypt docs) - #in a sane manner - #--------------------------- - - #config with "$" suffix, hash strings with "$$" suffix, - # should all be treated the same, with one "$" added to salt digest. - ("$md5$3UqYqndY$", - "this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), - ("$md5$3UqYqndY$$x", - "this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), - - #config with no suffix, hash strings with "$" suffix, - # should all be treated the same, and no suffix added to salt digest. - #NOTE: this is just a guess re: config w/ no suffix, - # but otherwise there's no sane way to encode bare_salt=False - # within config string. - ("$md5$RPgLF6IJ", - "passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), - ("$md5$RPgLF6IJ$x", - "passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), - ] - - known_malformed_hashes = [ - #bad char in otherwise correct hash - "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/", - - #2+ "$" at end of salt in config - #NOTE: not sure what correct behavior is, so forbidding format for now. - "$md5$3UqYqndY$$", - - #3+ "$" at end of salt in hash - #NOTE: not sure what correct behavior is, so forbidding format for now. - "$md5$RPgLa6IJ$$$WTvAlUJ7MqH5xak2FMEwS/", - - ] - -#========================================================= -#unix fallback -#========================================================= -from passlib.handlers.misc import unix_fallback - -class UnixFallbackTest(HandlerCase): - #NOTE: this class behaves VERY differently from a normal password hash, - #so we subclass & disable a number of the default tests. - #TODO: combine some of these features w/ django_disabled and other fallback handlers. - - handler = unix_fallback - - known_correct_hashes = [ ("passwordwc",""), ] - known_other_hashes = [] - accepts_empty_hash = True - - #NOTE: to ease testing, this sets enable_wildcard iff the string 'wc' is in the secret - - def do_verify(self, secret, hash): - return self.handler.verify(secret, hash, enable_wildcard='wc' in secret) - - def test_50_encrypt_plain(self): - "test encrypt() basic behavior" - secret = u"\u20AC\u00A5$" - result = self.do_encrypt(secret) - self.assertEqual(result, "!") - self.assertTrue(not self.do_verify(secret, result)) - - def test_wildcard(self): - "test enable_wildcard flag" - h = self.handler - self.assertTrue(h.verify('password','', enable_wildcard=True)) - self.assertFalse(h.verify('password','')) - for c in ("!*x"): - self.assertFalse(h.verify('password',c, enable_wildcard=True)) - self.assertFalse(h.verify('password',c)) - -#========================================================= -#EOF -#========================================================= diff -Nru passlib-1.5.3/passlib/tests/test_ext_django.py passlib-1.6.1/passlib/tests/test_ext_django.py --- passlib-1.5.3/passlib/tests/test_ext_django.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_ext_django.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,60 +1,69 @@ """test passlib.ext.django""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import logging; log = logging.getLogger(__name__) import sys import warnings -#site -#pkg -from passlib.context import CryptContext, CryptPolicy -from passlib.apps import django_context -from passlib.ext.django import utils -from passlib.hash import sha256_crypt -from passlib.tests.utils import TestCase, unittest, ut_version, catch_warnings -import passlib.tests.test_drivers as td -from passlib.utils import Undef +# site +# pkg +from passlib.apps import django10_context, django14_context +from passlib.context import CryptContext +import passlib.exc as exc +from passlib.utils.compat import iteritems, unicode, get_method_function, u, PY3 +from passlib.utils import memoized_property from passlib.registry import get_crypt_handler -#module - -#========================================================= -# import & configure django settings, -#========================================================= - -try: - from django.conf import settings, LazySettings - has_django = True -except ImportError: - settings = None - has_django = False - -has_django0 = False #are we using django 0.9 release? -has_django1 = False #inverse - are we using django >= 1.0 +# tests +from passlib.tests.utils import TestCase, skipUnless, catch_warnings, TEST_MODE, has_active_backend +from passlib.tests.test_handlers import get_handler_case +# local + +#============================================================================= +# configure django settings for testcases +#============================================================================= +from passlib.ext.django.utils import DJANGO_VERSION + +# disable all Django integration tests under py3, +# since Django doesn't support py3 yet. +if PY3 and DJANGO_VERSION < (1,5): + DJANGO_VERSION = () + +# convert django version to some cheap flags +has_django = bool(DJANGO_VERSION) +has_django0 = has_django and DJANGO_VERSION < (1,0) +has_django1 = DJANGO_VERSION >= (1,0) +has_django14 = DJANGO_VERSION >= (1,4) +# import and configure empty django settings if has_django: - from django import VERSION - has_django0 = (VERSION < (1,0)) - has_django1 = (VERSION >= (1,0)) + from django.conf import settings, LazySettings if not isinstance(settings, LazySettings): - #this could mean django has been configured somehow, - #which we don't want, since test cases reset and manipulate settings. + # this probably means django globals have been configured already, + # which we don't want, since test cases reset and manipulate settings. raise RuntimeError("expected django.conf.settings to be LazySettings: %r" % (settings,)) - #else configure a blank settings instance for our unittests + # else configure a blank settings instance for the unittests if has_django0: if settings._target is None: from django.conf import UserSettingsHolder, global_settings settings._target = UserSettingsHolder(global_settings) - else: - if not settings.configured: - settings.configure() + elif not settings.configured: + settings.configure() + +#============================================================================= +# support funcs +#============================================================================= + +# flag for update_settings() to remove specified key entirely +UNSET = object() def update_settings(**kwds): - for k,v in kwds.iteritems(): - if v is Undef: + """helper to update django settings from kwds""" + for k,v in iteritems(kwds): + if v is UNSET: if hasattr(settings, k): if has_django0: delattr(settings._target, k) @@ -63,509 +72,758 @@ else: setattr(settings, k, v) -#========================================================= -# and prepare helper to skip all relevant tests -# if django isn't installed. -#========================================================= -def skipUnlessDjango(cls): - "helper to skip class if django not present" - if has_django: - return cls - if ut_version < 2: - return None - return unittest.skip("Django not installed")(cls) - -#========================================================= -# mock user object -#========================================================= if has_django: - import django.contrib.auth.models as dam - - class FakeUser(dam.User): - "stub user object for testing" - #this mainly just overrides .save() to test commit behavior. + from django.contrib.auth.models import User - saved_password = None + class FakeUser(User): + "mock user object for use in testing" + # NOTE: this mainly just overrides .save() to test commit behavior. + + @memoized_property + def saved_passwords(self): + return [] + + def pop_saved_passwords(self): + try: + return self.saved_passwords[:] + finally: + del self.saved_passwords[:] def save(self): - self.saved_password = self.password + self.saved_passwords.append(self.password) -#========================================================= -# helper contexts -#========================================================= - -# simple context which looks NOTHING like django, -# so we can tell if patching worked. -simple_context = CryptContext( - schemes = [ "md5_crypt", "des_crypt" ], - default = "md5_crypt", - deprecated = [ "des_crypt" ], -) - -# some sample hashes -sample1 = 'password' -sample1_md5 = '$1$kAd49ifN$biuRAv1Tv0zGHyCv0uIqW.' -sample1_des = 'PPPTDkiCeu/jM' -sample1_sha1 = 'sha1$b215d$9ee0a66f84ef1ad99096355e788135f7e949bd41' - -# context for testing category funcs -category_context = CryptContext( - schemes = [ "sha256_crypt" ], - sha256_crypt__rounds = 1000, - staff__sha256_crypt__rounds = 2000, - superuser__sha256_crypt__rounds = 3000, -) - -def get_cc_rounds(**kwds): - "helper for testing category funcs" - user = FakeUser(**kwds) - user.set_password("placeholder") - return sha256_crypt.from_string(user.password).rounds +def create_mock_setter(): + state = [] + def setter(password): + state.append(password) + def popstate(): + try: + return state[:] + finally: + del state[:] + setter.popstate = popstate + return setter + +#============================================================================= +# work up stock django config +#============================================================================= +if has_django14: + # have to modify this a little - + # all but pbkdf2_sha256 will be deprecated here, + # whereas preconfigured passlib policy is more permissive + stock_config = django14_context.to_dict() + stock_config['deprecated'] = ["django_pbkdf2_sha1", "django_bcrypt"] + stock_config['deprecated'] +elif has_django1: + stock_config = django10_context.to_dict() +else: + # 0.9.6 config + stock_config = dict(schemes=["django_salted_sha1", "django_salted_md5", "hex_md5"], + deprecated=["hex_md5"]) -#========================================================= +#============================================================================= # test utils -#========================================================= -class PatchTest(TestCase): - "test passlib.ext.django.utils:set_django_password_context" - - case_prefix = "passlib.ext.django utils" - +#============================================================================= +class _ExtensionSupport(object): + "support funcs for loading/unloading extension" + #=================================================================== + # support funcs + #=================================================================== + @classmethod + def _iter_patch_candidates(cls): + """helper to scan for monkeypatches. + + returns tuple containing: + * object (module or class) + * attribute of object + * value of attribute + * whether it should or should not be patched + """ + # XXX: this and assert_unpatched() could probably be refactored to use + # the PatchManager class to do the heavy lifting. + from django.contrib.auth import models + user_attrs = ["check_password", "set_password"] + model_attrs = ["check_password"] + objs = [(models, model_attrs), (models.User, user_attrs)] + if has_django14: + from django.contrib.auth import hashers + model_attrs.append("make_password") + objs.append((hashers, ["check_password", "make_password", + "get_hasher", "identify_hasher"])) + if has_django0: + user_attrs.extend(["has_usable_password", "set_unusable_password"]) + for obj, patched in objs: + for attr in dir(obj): + if attr.startswith("_"): + continue + value = obj.__dict__.get(attr, UNSET) # can't use getattr() due to GAE + if value is UNSET and attr not in patched: + continue + value = get_method_function(value) + source = getattr(value, "__module__", None) + if source: + yield obj, attr, source, (attr in patched) + + #=================================================================== + # verify current patch state + #=================================================================== def assert_unpatched(self): - "helper to ensure django hasn't been patched" - state = utils._django_patch_state + "test that django is in unpatched state" + # make sure we aren't currently patched + mod = sys.modules.get("passlib.ext.django.models") + self.assertFalse(mod and mod._patched, "patch should not be enabled") + + # make sure no objects have been replaced, by checking __module__ + for obj, attr, source, patched in self._iter_patch_candidates(): + if patched: + self.assertTrue(source.startswith("django.contrib.auth."), + "obj=%r attr=%r was not reverted: %r" % + (obj, attr, source)) + else: + self.assertFalse(source.startswith("passlib."), + "obj=%r attr=%r should not have been patched: %r" % + (obj, attr, source)) + + def assert_patched(self, context=None): + "helper to ensure django HAS been patched, and is using specified config" + # make sure we're currently patched + mod = sys.modules.get("passlib.ext.django.models") + self.assertTrue(mod and mod._patched, "patch should have been enabled") + + # make sure only the expected objects have been patched + for obj, attr, source, patched in self._iter_patch_candidates(): + if patched: + self.assertTrue(source == "passlib.ext.django.models", + "obj=%r attr=%r should have been patched: %r" % + (obj, attr, source)) + else: + self.assertFalse(source.startswith("passlib."), + "obj=%r attr=%r should not have been patched: %r" % + (obj, attr, source)) + + # check context matches + if context is not None: + context = CryptContext._norm_source(context) + self.assertEqual(mod.password_context.to_dict(resolve=True), + context.to_dict(resolve=True)) + + #=================================================================== + # load / unload the extension (and verify it worked) + #=================================================================== + _config_keys = ["PASSLIB_CONFIG", "PASSLIB_CONTEXT", "PASSLIB_GET_CATEGORY"] + + def load_extension(self, check=True, **kwds): + "helper to load extension with specified config & patch django" + self.unload_extension() + if check: + config = kwds.get("PASSLIB_CONFIG") or kwds.get("PASSLIB_CONTEXT") + for key in self._config_keys: + kwds.setdefault(key, UNSET) + update_settings(**kwds) + import passlib.ext.django.models + if check: + self.assert_patched(context=config) - #make sure we aren't currently patched - self.assertIs(state, None) + def unload_extension(self): + "helper to remove patches and unload extension" + # remove patches and unload module + mod = sys.modules.get("passlib.ext.django.models") + if mod: + mod._remove_patch() + del sys.modules["passlib.ext.django.models"] + # wipe config from django settings + update_settings(**dict((key, UNSET) for key in self._config_keys)) + # check everything's gone + self.assert_unpatched() - #make sure nothing else patches django - for func in [ - dam.check_password, - dam.User.check_password, - dam.User.set_password, - ]: - self.assertEquals(func.__module__, "django.contrib.auth.models") - self.assertFalse(hasattr(dam.User, "password_context")) - - def assert_patched(self, context=Undef): - "helper to ensure django HAS been patched" - state = utils._django_patch_state - - #make sure we're patched - self.assertIsNot(state, None) - - #make sure our methods are exposed - for func in [ - dam.check_password, - dam.User.check_password, - dam.User.set_password, - ]: - self.assertEquals(func.__module__, "passlib.ext.django.utils") - - #make sure methods match - self.assertIs(dam.check_password, state['models_check_password']) - self.assertIs(dam.User.check_password.im_func, state['user_check_password']) - self.assertIs(dam.User.set_password.im_func, state['user_set_password']) - - #make sure context matches - obj = dam.User.password_context - self.assertIs(obj, state['context']) - if context is not Undef: - self.assertIs(obj, context) - - #make sure old methods were stored - for key in [ - "orig_models_check_password", - "orig_user_check_password", - "orig_user_set_password", - ]: - value = state[key] - self.assertEquals(value.__module__, "django.contrib.auth.models") + #=================================================================== + # eoc + #=================================================================== +class _ExtensionTest(TestCase, _ExtensionSupport): def setUp(self): - #reset to baseline, and verify - utils.set_django_password_context(None) - self.assert_unpatched() - - def tearDown(self): - #reset to baseline, and verify - utils.set_django_password_context(None) - self.assert_unpatched() + super(_ExtensionTest, self).setUp() - def test_00_patch_control(self): - "test set_django_password_context patch/unpatch" + self.require_TEST_MODE("default") - #check context=None has no effect - utils.set_django_password_context(None) - self.assert_unpatched() + if not has_django: + raise self.skipTest("Django not installed") - #patch to use stock django context - utils.set_django_password_context(django_context) - self.assert_patched(context=django_context) + # reset to baseline, and verify it worked + self.unload_extension() + + # and do the same when the test exits + self.addCleanup(self.unload_extension) + +#============================================================================= +# extension tests +#============================================================================= +class DjangoBehaviorTest(_ExtensionTest): + "tests model to verify it matches django's behavior" + descriptionPrefix = "verify django behavior" + patched = False + config = stock_config + + # NOTE: if this test fails, it means we're not accounting for + # some part of django's hashing logic, or that this is + # running against an untested version of django with a new + # hashing policy. + + @property + def context(self): + return CryptContext._norm_source(self.config) + + def assert_unusable_password(self, user): + self.assertEqual(user.password, "!") + if has_django1 or self.patched: + self.assertFalse(user.has_usable_password()) + self.assertEqual(user.pop_saved_passwords(), []) - #try to remove patch - utils.set_django_password_context(None) - self.assert_unpatched() + def assert_valid_password(self, user, hash=UNSET, saved=None): + if hash is UNSET: + self.assertNotEqual(user.password, "!") + self.assertNotEqual(user.password, None) + else: + self.assertEqual(user.password, hash) + if has_django1 or self.patched: + self.assertTrue(user.has_usable_password()) + self.assertEqual(user.pop_saved_passwords(), + [] if saved is None else [saved]) - #patch to use stock django context again - utils.set_django_password_context(django_context) - self.assert_patched(context=django_context) + def test_config(self): + """test hashing interface - #try to remove patch again - utils.set_django_password_context(None) - self.assert_unpatched() + this function is run against both the actual django code, to + verify the assumptions of the unittests are correct; + and run against the passlib extension, to verify it matches + those assumptions. + """ + patched, config = self.patched, self.config + # this tests the following methods: + # User.set_password() + # User.check_password() + # make_password() -- 1.4 only + # check_password() + # identify_hasher() + # User.has_usable_password() + # User.set_unusable_password() + # XXX: this take a while to run. what could be trimmed? + + # TODO: get_hasher() + + #======================================================= + # setup helpers & imports + #======================================================= + ctx = self.context + setter = create_mock_setter() + PASS1 = "toomanysecrets" + WRONG1 = "letmein" + + if has_django14: + from passlib.ext.django.utils import hasher_to_passlib_name, passlib_to_hasher_name + from django.contrib.auth.hashers import check_password, make_password, is_password_usable + if patched: + from django.contrib.auth.hashers import identify_hasher + else: + from django.contrib.auth.models import check_password - def test_01_patch_control_detection(self): - "test set_django_password_context detection of foreign monkeypatches" - def dummy(): - pass + #======================================================= + # make sure extension is configured correctly + #======================================================= + if patched: + # contexts should match + from passlib.ext.django.models import password_context + self.assertEqual(password_context.to_dict(resolve=True), + ctx.to_dict(resolve=True)) + + # should have patched both places + if has_django14: + from django.contrib.auth.models import check_password as check_password2 + self.assertIs(check_password2, check_password) + + #======================================================= + # default algorithm + #======================================================= + # User.set_password() should use default alg + user = FakeUser() + user.set_password(PASS1) + self.assertTrue(ctx.handler().verify(PASS1, user.password)) + self.assert_valid_password(user) + + # User.check_password() - n/a + + # make_password() should use default alg + if has_django14: + hash = make_password(PASS1) + self.assertTrue(ctx.handler().verify(PASS1, hash)) + + # check_password() - n/a + + #======================================================= + # empty password behavior + #======================================================= + if has_django14: + # NOTE: django 1.4 treats empty password as invalid + + # User.set_password() should set unusable flag + user = FakeUser() + user.set_password('') + self.assert_unusable_password(user) + + # User.check_password() should never return True + user = FakeUser() + user.password = hash = ctx.encrypt("") + self.assertFalse(user.check_password("")) + self.assert_valid_password(user, hash) - with catch_warnings(record=True) as wlog: - warnings.simplefilter("always") + # make_password() should reject empty passwords + self.assertEqual(make_password(""), "!") - #patch to use stock django context - utils.set_django_password_context(django_context) - self.assert_patched(context=django_context) - self.assertEquals(len(wlog), 0) - - #mess with User.set_password, make sure it's detected - dam.User.set_password = dummy - utils.set_django_password_context(django_context) - self.assert_patched(context=django_context) - self.assertEquals(len(wlog), 1) - self.assertWarningMatches(wlog.pop(), - message_re="^another library has patched.*User\.set_password$") - - #mess with user.check_password, make sure it's detected - dam.User.check_password = dummy - utils.set_django_password_context(django_context) - self.assert_patched(context=django_context) - self.assertEquals(len(wlog), 1) - self.assertWarningMatches(wlog.pop(), - message_re="^another library has patched.*User\.check_password$") - - #mess with user.check_password, make sure it's detected - dam.check_password = dummy - utils.set_django_password_context(django_context) - self.assert_patched(context=django_context) - self.assertEquals(len(wlog), 1) - self.assertWarningMatches(wlog.pop(), - message_re="^another library has patched.*models:check_password$") - - def test_01_patch_bad_types(self): - "test set_django_password_context bad inputs" - set = utils.set_django_password_context - self.assertRaises(TypeError, set, CryptPolicy()) - self.assertRaises(TypeError, set, "") - - def test_02_models_check_password(self): - "test monkeypatched models.check_password()" - - # patch to use simple context - utils.set_django_password_context(simple_context) - self.assert_patched(context=simple_context) - - # check correct hashes pass - self.assertTrue(dam.check_password(sample1, sample1_des)) - self.assertTrue(dam.check_password(sample1, sample1_md5)) - - # check bad password fail w/ false - self.assertFalse(dam.check_password('x', sample1_des)) - self.assertFalse(dam.check_password('x', sample1_md5)) - - # and other hashes fail w/ error - self.assertRaises(ValueError, dam.check_password, sample1, sample1_sha1) - self.assertRaises(ValueError, dam.check_password, sample1, None) - - def test_03_check_password(self): - "test monkeypatched User.check_password()" - # NOTE: using FakeUser so we can test .save() - user = FakeUser() + # check_password() should never return True + self.assertFalse(check_password("", hash)) - # patch to use simple context - utils.set_django_password_context(simple_context) - self.assert_patched(context=simple_context) - - # test that blank hash is never accepted - self.assertEqual(user.password, '') - self.assertIs(user.saved_password, None) - self.assertFalse(user.check_password('x')) - - # check correct secrets pass, and wrong ones fail - user.password = sample1_md5 - self.assertTrue(user.check_password(sample1)) - self.assertFalse(user.check_password('x')) - self.assertFalse(user.check_password(None)) - - # none of that should have triggered update of password - self.assertEqual(user.password, sample1_md5) - self.assertIs(user.saved_password, None) + else: + # User.set_password() should use default alg + user = FakeUser() + user.set_password('') + hash = user.password + self.assertTrue(ctx.handler().verify('', hash)) + self.assert_valid_password(user, hash) + + # User.check_password() should return True + self.assertTrue(user.check_password("")) + self.assert_valid_password(user, hash) + + # no make_password() + + # check_password() should return True + self.assertTrue(check_password("", hash)) + + #======================================================= + # 'unusable flag' behavior + #======================================================= + if has_django1 or patched: - #check unusable password - if has_django1: + # sanity check via user.set_unusable_password() + user = FakeUser() user.set_unusable_password() - self.assertFalse(user.has_usable_password()) + self.assert_unusable_password(user) + + # ensure User.set_password() sets flag + user = FakeUser() + user.set_password(None) + self.assert_unusable_password(user) + + # User.check_password() should always fail self.assertFalse(user.check_password(None)) self.assertFalse(user.check_password('')) - self.assertFalse(user.check_password(sample1)) + self.assertFalse(user.check_password(PASS1)) + self.assertFalse(user.check_password(WRONG1)) + self.assert_unusable_password(user) + + # make_password() should also set flag + if has_django14: + self.assertEqual(make_password(None), "!") + + # check_password() should return False (didn't handle disabled under 1.3) + if has_django14 or patched: + self.assertFalse(check_password(PASS1, '!')) + + # identify_hasher() and is_password_usable() should reject it + if has_django14: + self.assertFalse(is_password_usable(user.password)) + if has_django14 and patched: + self.assertRaises(ValueError, identify_hasher, user.password) + + #======================================================= + # hash=None + #======================================================= + # User.set_password() - n/a - def test_04_check_password_migration(self): - "test User.check_password() hash migration" - # NOTE: using FakeUser so we can test .save() + # User.check_password() - returns False user = FakeUser() + user.password = None + if has_django14 or patched: + self.assertFalse(user.check_password(PASS1)) + else: + self.assertRaises(TypeError, user.check_password, PASS1) + if has_django1 or patched: + self.assertFalse(user.has_usable_password()) - # patch to use simple context - utils.set_django_password_context(simple_context) - self.assert_patched(context=simple_context) - - # set things up with a password that needs migration - user.password = sample1_des - self.assertEqual(user.password, sample1_des) - self.assertIs(user.saved_password, None) - - # run check with bad password... - # shouldn't have migrated - self.assertFalse(user.check_password('x')) - self.assertFalse(user.check_password(None)) - - self.assertEqual(user.password, sample1_des) - self.assertIs(user.saved_password, None) - - # run check with correct password... - # should have migrated to md5 and called save() - self.assertTrue(user.check_password(sample1)) - - self.assertTrue(user.password.startswith("$1$")) - self.assertEqual(user.saved_password, user.password) - - # check resave doesn't happen - user.saved_password = None - self.assertTrue(user.check_password(sample1)) - self.assertIs(user.saved_password, None) + # make_password() - n/a - def test_05_set_password(self): - "test monkeypatched User.set_password()" - user = FakeUser() + # check_password() - error + if has_django14 or patched: + self.assertFalse(check_password(PASS1, None)) + else: + self.assertRaises(AttributeError, check_password, PASS1, None) - # patch to use simple context - utils.set_django_password_context(simple_context) - self.assert_patched(context=simple_context) - - # sanity check - self.assertEqual(user.password, '') - self.assertIs(user.saved_password, None) - if has_django1: - self.assertTrue(user.has_usable_password()) + # identify_hasher() - error + if has_django14 and patched: + self.assertRaises(TypeError, identify_hasher, None) + + #======================================================= + # invalid hash values + #======================================================= + for hash in ("", "$789$foo"): + # User.set_password() - n/a + + # User.check_password() - blank hash causes error + user = FakeUser() + user.password = hash + if has_django14 or patched or hash: + self.assertRaises(ValueError, user.check_password, PASS1) + else: + # django 1.3 returns False for empty hashes + self.assertFalse(user.check_password(PASS1)) + self.assert_valid_password(user, hash) # '' counts as valid for some reason + + # make_password() - n/a + + # check_password() - error + self.assertRaises(ValueError, check_password, PASS1, hash) + + # identify_hasher() - error + if has_django14 and patched: + self.assertRaises(ValueError, identify_hasher, hash) + + #======================================================= + # run through all the schemes in the context, + # testing various bits of per-scheme behavior. + #======================================================= + for scheme in ctx.schemes(): + #------------------------------------------------------- + # setup constants & imports, pick a sample secret/hash combo + #------------------------------------------------------- + handler = ctx.handler(scheme) + deprecated = ctx._is_deprecated_scheme(scheme) + assert not deprecated or scheme != ctx.default_scheme() + try: + testcase = get_handler_case(scheme) + except exc.MissingBackendError: + assert scheme == "bcrypt" + continue + assert testcase.handler is handler + if testcase.is_disabled_handler: + continue + if not has_active_backend(handler): + assert scheme == "django_bcrypt" + continue + while True: + secret, hash = testcase('setUp').get_sample_hash() + if secret: # don't select blank passwords, special under django + break + other = 'letmein' + + # User.set_password() - n/a + + #------------------------------------------------------- + # User.check_password()+migration against known hash + #------------------------------------------------------- + user = FakeUser() + user.password = hash + + # check against invalid password + if has_django1 or patched: + self.assertFalse(user.check_password(None)) + else: + self.assertRaises(TypeError, user.check_password, None) + ##self.assertFalse(user.check_password('')) + self.assertFalse(user.check_password(other)) + self.assert_valid_password(user, hash) + + # check against valid password + if has_django0 and isinstance(secret, unicode): + secret = secret.encode("utf-8") + self.assertTrue(user.check_password(secret)) + + # check if it upgraded the hash + needs_update = deprecated + if needs_update: + self.assertFalse(handler.identify(user.password)) + self.assertTrue(ctx.handler().verify(secret, user.password)) + self.assert_valid_password(user, saved=user.password) + else: + self.assert_valid_password(user, hash) + + # don't need to check rest for most deployments + if TEST_MODE(max="default"): + continue + + #------------------------------------------------------- + # make_password() correctly selects algorithm + #------------------------------------------------------- + if has_django14: + hash2 = make_password(secret, hasher=passlib_to_hasher_name(scheme)) + self.assertTrue(handler.verify(secret, hash2)) + + #------------------------------------------------------- + # check_password()+setter against known hash + #------------------------------------------------------- + if has_django14 or patched: + # should call setter only if it needs_update + self.assertTrue(check_password(secret, hash, setter=setter)) + self.assertEqual(setter.popstate(), [secret] if needs_update else []) + + # should not call setter + self.assertFalse(check_password(other, hash, setter=setter)) + self.assertEqual(setter.popstate(), []) + + ### check preferred kwd is ignored (django 1.4 feature we don't support) + ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey')) + ##self.assertEqual(setter.popstate(), [secret]) + + elif patched or scheme != "hex_md5": + # django 1.3 never called check_password() for hex_md5 + self.assertTrue(check_password(secret, hash)) + self.assertFalse(check_password(other, hash)) + + # TODO: get_hasher() + + #------------------------------------------------------- + # identify_hasher() recognizes known hash + #------------------------------------------------------- + if has_django14 and patched: + self.assertTrue(is_password_usable(hash)) + name = hasher_to_passlib_name(identify_hasher(hash).algorithm) + self.assertEqual(name, scheme) + +class ExtensionBehaviorTest(DjangoBehaviorTest): + "test model to verify passlib.ext.django conforms to it" + descriptionPrefix = "verify extension behavior" + patched = True + config = dict( + schemes="sha256_crypt,md5_crypt,des_crypt", + deprecated="des_crypt", + ) - # set password - user.set_password(sample1) - self.assertTrue(user.check_password(sample1)) - self.assertEquals(simple_context.identify(user.password), "md5_crypt") - self.assertIs(user.saved_password, None) - - #check unusable password - user.set_password(None) - if has_django1: - self.assertFalse(user.has_usable_password()) - self.assertIs(user.saved_password, None) + def setUp(self): + super(ExtensionBehaviorTest, self).setUp() + self.load_extension(PASSLIB_CONFIG=self.config) - def test_06_get_category(self): - "test default get_category function" - func = utils.get_category - self.assertIs(func(FakeUser()), None) - self.assertEquals(func(FakeUser(is_staff=True)), "staff") - self.assertEquals(func(FakeUser(is_superuser=True)), "superuser") - self.assertEquals(func(FakeUser(is_staff=True, - is_superuser=True)), "superuser") - - def test_07_get_category(self): - "test set_django_password_context's get_category parameter" - # test patch uses default get_category - utils.set_django_password_context(category_context) - self.assertEquals(get_cc_rounds(), 1000) - self.assertEquals(get_cc_rounds(is_staff=True), 2000) - self.assertEquals(get_cc_rounds(is_superuser=True), 3000) +class DjangoExtensionTest(_ExtensionTest): + """test the ``passlib.ext.django`` plugin""" + descriptionPrefix = "passlib.ext.django plugin" + + #=================================================================== + # monkeypatch testing + #=================================================================== + def test_00_patch_control(self): + "test set_django_password_context patch/unpatch" - # test patch uses explicit get_category - def get_category(user): - return user.first_name or None - utils.set_django_password_context(category_context, get_category) - self.assertEquals(get_cc_rounds(), 1000) - self.assertEquals(get_cc_rounds(first_name='other'), 1000) - self.assertEquals(get_cc_rounds(first_name='staff'), 2000) - self.assertEquals(get_cc_rounds(first_name='superuser'), 3000) - - # test patch can disable get_category - utils.set_django_password_context(category_context, None) - self.assertEquals(get_cc_rounds(), 1000) - self.assertEquals(get_cc_rounds(first_name='other'), 1000) - self.assertEquals(get_cc_rounds(first_name='staff', is_staff=True), 1000) - self.assertEquals(get_cc_rounds(first_name='superuser', is_superuser=True), 1000) - -PatchTest = skipUnlessDjango(PatchTest) - -#========================================================= -# test django plugin -#========================================================= - -django_hash_tests = [ - td.HexMd5Test, - td.DjangoDesCryptTest, - td.DjangoSaltedMd5Test, - td.DjangoSaltedSha1Test, - ] - -default_hash_tests = django_hash_tests + [ td.Builtin_SHA512CryptTest ] + # check config="disabled" + self.load_extension(PASSLIB_CONFIG="disabled", check=False) + self.assert_unpatched() -if has_django0: - django_hash_tests.remove(td.DjangoDesCryptTest) + # check legacy config=None + with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"): + self.load_extension(PASSLIB_CONFIG=None, check=False) + self.assert_unpatched() -class PluginTest(TestCase): - "test django plugin via settings" + # try stock django 1.0 context + self.load_extension(PASSLIB_CONFIG="django-1.0", check=False) + self.assert_patched(context=django10_context) + + # try to remove patch + self.unload_extension() + + # patch to use stock django 1.4 context + self.load_extension(PASSLIB_CONFIG="django-1.4", check=False) + self.assert_patched(context=django14_context) + + # try to remove patch again + self.unload_extension() + + def test_01_overwrite_detection(self): + "test detection of foreign monkeypatching" + # NOTE: this sets things up, and spot checks two methods, + # this should be enough to verify patch manager is working. + # TODO: test unpatch behavior honors flag. + + # configure plugin to use sample context + config = "[passlib]\nschemes=des_crypt\n" + self.load_extension(PASSLIB_CONFIG=config) + + # setup helpers + import django.contrib.auth.models as models + from passlib.ext.django.models import _manager + def dummy(): + pass - case_prefix = "passlib.ext.django plugin" + # mess with User.set_password, make sure it's detected + orig = models.User.set_password + models.User.set_password = dummy + with self.assertWarningList("another library has patched.*User\.set_password"): + _manager.check_all() + models.User.set_password = orig + + # mess with models.check_password, make sure it's detected + orig = models.check_password + models.check_password = dummy + with self.assertWarningList("another library has patched.*models:check_password"): + _manager.check_all() + models.check_password = orig + + def test_02_handler_wrapper(self): + "test Hasher-compatible handler wrappers" + if not has_django14: + raise self.skipTest("Django >= 1.4 not installed") + from passlib.ext.django.utils import get_passlib_hasher + from django.contrib.auth import hashers + + # should return native django hasher if available + hasher = get_passlib_hasher("hex_md5") + self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher) + + hasher = get_passlib_hasher("django_bcrypt") + self.assertIsInstance(hasher, hashers.BCryptPasswordHasher) + + # otherwise should return wrapper + from passlib.hash import sha256_crypt + hasher = get_passlib_hasher("sha256_crypt") + self.assertEqual(hasher.algorithm, "passlib_sha256_crypt") + + # and wrapper should return correct hash + encoded = hasher.encode("stub") + self.assertTrue(sha256_crypt.verify("stub", encoded)) + self.assertTrue(hasher.verify("stub", encoded)) + self.assertFalse(hasher.verify("xxxx", encoded)) + + # test wrapper accepts options + encoded = hasher.encode("stub", "abcd"*4, iterations=1234) + self.assertEqual(encoded, "$5$rounds=1234$abcdabcdabcdabcd$" + "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6") + self.assertEqual(hasher.safe_summary(encoded), + {'algorithm': 'sha256_crypt', + 'salt': u('abcdab**********'), + 'iterations': 1234, + 'hash': u('v2RWkZ*************************************'), + }) + + #=================================================================== + # PASSLIB_CONFIG settings + #=================================================================== + def test_11_config_disabled(self): + "test PASSLIB_CONFIG='disabled'" + # test config=None (deprecated) + with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"): + self.load_extension(PASSLIB_CONFIG=None, check=False) + self.assert_unpatched() - def setUp(self): - #remove django patch - utils.set_django_password_context(None) + # test disabled config + self.load_extension(PASSLIB_CONFIG="disabled", check=False) + self.assert_unpatched() - #ensure django settings are empty - update_settings( - PASSLIB_CONTEXT=Undef, - PASSLIB_GET_CATEGORY=Undef, - ) - - #unload module so it's re-run - sys.modules.pop("passlib.ext.django.models", None) - - def tearDown(self): - #remove django patch - utils.set_django_password_context(None) - - def check_hashes(self, tests, new_hash=None, deprecated=None): - u = FakeUser() - deprecated = None - - # check new hash construction - if new_hash: - u.set_password("placeholder") - handler = get_crypt_handler(new_hash) - self.assertTrue(handler.identify(u.password)) - - # run against hashes from tests... - for test in tests: - for secret, hash in test.all_correct_hashes: - - # check against valid password - u.password = hash - if has_django0 and isinstance(secret, unicode): - secret = secret.encode("utf-8") - self.assertTrue(u.check_password(secret)) - if new_hash and deprecated and test.handler.name in deprecated: - self.assertFalse(handler.identify(hash)) - self.assertTrue(handler.identify(u.password)) - - # check against invalid password - u.password = hash - self.assertFalse(u.check_password('x'+secret)) - if new_hash and deprecated and test.handler.name in deprecated: - self.assertFalse(handler.identify(hash)) - self.assertEquals(u.password, hash) - - # check disabled handling - if has_django1: - u.set_password(None) - handler = get_crypt_handler("django_disabled") - self.assertTrue(handler.identify(u.password)) - self.assertFalse(u.check_password('placeholder')) - - def test_00_actual_django(self): - "test actual Django behavior has not changed" - #NOTE: if this test fails, - # probably means newer version of Django, - # and passlib's policies should be updated. - self.check_hashes(django_hash_tests, - "django_salted_sha1", - ["hex_md5"]) - - def test_01_explicit_unset(self, value=None): - "test PASSLIB_CONTEXT = None" - update_settings( - PASSLIB_CONTEXT=value, - ) - import passlib.ext.django.models - self.check_hashes(django_hash_tests, - "django_salted_sha1", - ["hex_md5"]) - - def test_02_stock_ctx(self): - "test PASSLIB_CONTEXT = utils.STOCK_CTX" - self.test_01_explicit_unset(value=utils.STOCK_CTX) + def test_12_config_presets(self): + "test PASSLIB_CONFIG=''" + # test django presets + self.load_extension(PASSLIB_CONTEXT="django-default", check=False) + if has_django14: + ctx = django14_context + else: + ctx = django10_context + self.assert_patched(ctx) - def test_03_implicit_default_ctx(self): - "test PASSLIB_CONTEXT unset" - import passlib.ext.django.models - self.check_hashes(default_hash_tests, - "sha512_crypt", - ["hex_md5", "django_salted_sha1", - "django_salted_md5", - "django_des_crypt", - ]) - - def test_04_explicit_default_ctx(self): - "test PASSLIB_CONTEXT = utils.DEFAULT_CTX" - update_settings( - PASSLIB_CONTEXT=utils.DEFAULT_CTX, - ) - self.test_03_implicit_default_ctx() - - def test_05_default_ctx_alias(self): - "test PASSLIB_CONTEXT = 'passlib-default'" - update_settings( - PASSLIB_CONTEXT="passlib-default", - ) - self.test_03_implicit_default_ctx() - - def test_06_categories(self): - "test PASSLIB_GET_CATEGORY unset" - update_settings( - PASSLIB_CONTEXT=category_context.policy, - ) - import passlib.ext.django.models + self.load_extension(PASSLIB_CONFIG="django-1.0", check=False) + self.assert_patched(django10_context) - self.assertEquals(get_cc_rounds(), 1000) - self.assertEquals(get_cc_rounds(is_staff=True), 2000) - self.assertEquals(get_cc_rounds(is_superuser=True), 3000) + self.load_extension(PASSLIB_CONFIG="django-1.4", check=False) + self.assert_patched(django14_context) - def test_07_categories_explicit(self): - "test PASSLIB_GET_CATEGORY = function" + def test_13_config_defaults(self): + "test PASSLIB_CONFIG default behavior" + # check implicit default + from passlib.ext.django.utils import PASSLIB_DEFAULT + default = CryptContext.from_string(PASSLIB_DEFAULT) + self.load_extension() + self.assert_patched(PASSLIB_DEFAULT) + + # check default preset + self.load_extension(PASSLIB_CONTEXT="passlib-default", check=False) + self.assert_patched(PASSLIB_DEFAULT) + + # check explicit string + self.load_extension(PASSLIB_CONTEXT=PASSLIB_DEFAULT, check=False) + self.assert_patched(PASSLIB_DEFAULT) + + def test_14_config_invalid(self): + "test PASSLIB_CONFIG type checks" + update_settings(PASSLIB_CONTEXT=123, PASSLIB_CONFIG=UNSET) + self.assertRaises(TypeError, __import__, 'passlib.ext.django.models') + + self.unload_extension() + update_settings(PASSLIB_CONFIG="missing-preset", PASSLIB_CONTEXT=UNSET) + self.assertRaises(ValueError, __import__, 'passlib.ext.django.models') + + #=================================================================== + # PASSLIB_GET_CATEGORY setting + #=================================================================== + def test_21_category_setting(self): + "test PASSLIB_GET_CATEGORY parameter" + # define config where rounds can be used to detect category + config = dict( + schemes = ["sha256_crypt"], + sha256_crypt__default_rounds = 1000, + staff__sha256_crypt__default_rounds = 2000, + superuser__sha256_crypt__default_rounds = 3000, + ) + from passlib.hash import sha256_crypt + + def run(**kwds): + "helper to take in user opts, return rounds used in password" + user = FakeUser(**kwds) + user.set_password("stub") + return sha256_crypt.from_string(user.password).rounds + + # test default get_category + self.load_extension(PASSLIB_CONFIG=config) + self.assertEqual(run(), 1000) + self.assertEqual(run(is_staff=True), 2000) + self.assertEqual(run(is_superuser=True), 3000) + + # test patch uses explicit get_category function def get_category(user): return user.first_name or None - update_settings( - PASSLIB_CONTEXT = category_context.policy, - PASSLIB_GET_CATEGORY = get_category, - ) - import passlib.ext.django.models + self.load_extension(PASSLIB_CONTEXT=config, + PASSLIB_GET_CATEGORY=get_category) + self.assertEqual(run(), 1000) + self.assertEqual(run(first_name='other'), 1000) + self.assertEqual(run(first_name='staff'), 2000) + self.assertEqual(run(first_name='superuser'), 3000) - self.assertEquals(get_cc_rounds(), 1000) - self.assertEquals(get_cc_rounds(first_name='other'), 1000) - self.assertEquals(get_cc_rounds(first_name='staff'), 2000) - self.assertEquals(get_cc_rounds(first_name='superuser'), 3000) - - def test_08_categories_disabled(self): - "test PASSLIB_GET_CATEGORY = None" - update_settings( - PASSLIB_CONTEXT = category_context.policy, - PASSLIB_GET_CATEGORY = None, - ) - import passlib.ext.django.models - - self.assertEquals(get_cc_rounds(), 1000) - self.assertEquals(get_cc_rounds(first_name='other'), 1000) - self.assertEquals(get_cc_rounds(first_name='staff', is_staff=True), 1000) - self.assertEquals(get_cc_rounds(first_name='superuser', is_superuser=True), 1000) - -PluginTest = skipUnlessDjango(PluginTest) - -#========================================================= -#eof -#========================================================= + # test patch can disable get_category entirely + def get_category(user): + return None + self.load_extension(PASSLIB_CONTEXT=config, + PASSLIB_GET_CATEGORY=get_category) + self.assertEqual(run(), 1000) + self.assertEqual(run(first_name='other'), 1000) + self.assertEqual(run(first_name='staff', is_staff=True), 1000) + self.assertEqual(run(first_name='superuser', is_superuser=True), 1000) + + # test bad value + self.assertRaises(TypeError, self.load_extension, PASSLIB_CONTEXT=config, + PASSLIB_GET_CATEGORY='x') + + #=================================================================== + # eoc + #=================================================================== + +# hack up the some of the real django tests to run w/ extension loaded, +# to ensure we mimic their behavior. +if has_django14: + from django.contrib.auth.tests.hashers import TestUtilsHashPass as _TestHashers + class HashersTest(_TestHashers, _ExtensionSupport): + def setUp(self): + # omitted orig setup, loading hashers our own way + self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) + def tearDown(self): + self.unload_extension() + super(HashersTest, self).tearDown() + del _TestHashers + + HashersTest = skipUnless(TEST_MODE("default"), + "requires >= 'default' test mode")(HashersTest) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_handlers.py passlib-1.6.1/passlib/tests/test_handlers.py --- passlib-1.5.3/passlib/tests/test_handlers.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_handlers.py 2012-08-02 17:38:45.000000000 +0000 @@ -0,0 +1,2766 @@ +"""passlib.tests.test_handlers - tests for passlib hash algorithms""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import hashlib +import logging; log = logging.getLogger(__name__) +import os +import warnings +# site +# pkg +from passlib import hash +from passlib.utils import repeat_string +from passlib.utils.compat import irange, PY3, u, get_method_function +from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \ + TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin +# module + +#============================================================================= +# constants & support +#============================================================================= + +# some common unicode passwords which used as test cases +UPASS_WAV = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2') +UPASS_USD = u("\u20AC\u00A5$") +UPASS_TABLE = u("t\u00e1\u0411\u2113\u0259") + +PASS_TABLE_UTF8 = b('t\xc3\xa1\xd0\x91\xe2\x84\x93\xc9\x99') # utf-8 + +def get_handler_case(scheme): + "return HandlerCase instance for scheme, used by other tests" + from passlib.registry import get_crypt_handler + handler = get_crypt_handler(scheme) + if hasattr(handler, "backends") and not hasattr(handler, "wrapped"): + backend = handler.get_backend() + name = "%s_%s_test" % (scheme, backend) + else: + name = "%s_test" % scheme + return globals()[name] + +#============================================================================= +# apr md5 crypt +#============================================================================= +class apr_md5_crypt_test(HandlerCase): + handler = hash.apr_md5_crypt + + known_correct_hashes = [ + # + # http://httpd.apache.org/docs/2.2/misc/password_encryptions.html + # + ('myPassword', '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA/'), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$apr1$bzYrOHUx$a1FcpXuQDJV3vPY20CS6N1'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash ----\/ + '$apr1$r31.....$HqJZimcKQFAMYayBlzkrA!' + ] + +#============================================================================= +# bcrypt +#============================================================================= +class _bcrypt_test(HandlerCase): + "base for BCrypt test cases" + handler = hash.bcrypt + secret_size = 72 + reduce_default_rounds = True + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'), + ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'), + ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'), + ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'), + ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'), + + # + # test vectors from http://www.openwall.com/crypt v1.2 + # note that this omits any hashes that depend on crypt_blowfish's + # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password, + # and any 2x hashes); and only contain hashes which are correct + # under both crypt_blowfish 1.2 AND OpenBSD. + # + ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'), + ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'), + ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'), + ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'), + ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789chars after 72 are ignored', + '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'), + (b('\xa3'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), + (b('\xff\xa3345'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'), + (b('\xa3ab'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'), + (b('\xaa')*72 + b('chars after 72 are ignored as usual'), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'), + (b('\xaa\x55'*36), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'), + (b('\x55\xaa\xff'*24), + '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'), + + # keeping one of their 2y tests, because we are supporting that. + (b('\xa3'), + '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), + + # + # from py-bcrypt tests + # + ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), + ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), + ('abcdefghijklmnopqrstuvwxyz', + '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', + '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), + + # + # custom test vectors + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, + '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + ] + + if TEST_MODE("full"): + # + # add some extra tests related to 2/2a + # + CONFIG_2 = '$2$05$' + '.'*22 + CONFIG_A = '$2a$05$' + '.'*22 + known_correct_hashes.extend([ + ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), + ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), + ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'), + ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'), + ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), + ]) + + known_correct_configs = [ + ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE, + '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'), + ] + + known_unidentified_hashes = [ + # invalid minor version + "$2b$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + # \/ + "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + + # unsupported (but recognized) minor version + "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", + + # rounds not zero-padded (py-bcrypt rejects this, therefore so do we) + '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.' + + # NOTE: salts with padding bits set are technically malformed, + # but we can reliably correct & issue a warning for that. + ] + + platform_crypt_support = [ + ("freedbsd|openbsd|netbsd", True), + ("darwin", False), + # linux - may be present via addon, e.g. debian's libpam-unix2 + # solaris - depends on policy + ] + + #=================================================================== + # override some methods + #=================================================================== + def setUp(self): + # ensure builtin is enabled for duration of test. + if TEST_MODE("full") and self.backend == "builtin": + key = "PASSLIB_BUILTIN_BCRYPT" + orig = os.environ.get(key) + if orig: + self.addCleanup(os.environ.__setitem__, key, orig) + else: + self.addCleanup(os.environ.__delitem__, key) + os.environ[key] = "enabled" + super(_bcrypt_test, self).setUp() + + def populate_settings(self, kwds): + # builtin is still just way too slow. + if self.backend == "builtin": + kwds.setdefault("rounds", 4) + super(_bcrypt_test, self).populate_settings(kwds) + + #=================================================================== + # fuzz testing + #=================================================================== + def os_supports_ident(self, hash): + "check if OS crypt is expected to support given ident" + if hash is None: + return True + # most OSes won't support 2x/2y + # XXX: definitely not the BSDs, but what about the linux variants? + from passlib.handlers.bcrypt import IDENT_2X, IDENT_2Y + if hash.startswith(IDENT_2X) or hash.startswith(IDENT_2Y): + return False + return True + + def fuzz_verifier_pybcrypt(self): + # test against py-bcrypt if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y + from passlib.utils import to_native_str + try: + from bcrypt import hashpw + except ImportError: + return + def check_pybcrypt(secret, hash): + "pybcrypt" + secret = to_native_str(secret, self.fuzz_password_encoding) + if hash.startswith(IDENT_2Y): + hash = IDENT_2A + hash[4:] + try: + return hashpw(secret, hash) == hash + except ValueError: + raise ValueError("py-bcrypt rejected hash: %r" % (hash,)) + return check_pybcrypt + + def fuzz_verifier_bcryptor(self): + # test against bcryptor if available + from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y + from passlib.utils import to_native_str + try: + from bcryptor.engine import Engine + except ImportError: + return + def check_bcryptor(secret, hash): + "bcryptor" + secret = to_native_str(secret, self.fuzz_password_encoding) + if hash.startswith(IDENT_2Y): + hash = IDENT_2A + hash[4:] + elif hash.startswith(IDENT_2): + # bcryptor doesn't support $2$ hashes; but we can fake it + # using the $2a$ algorithm, by repeating the password until + # it's 72 chars in length. + hash = IDENT_2A + hash[3:] + if secret: + secret = repeat_string(secret, 72) + return Engine(False).hash_key(secret, hash) == hash + return check_bcryptor + + def get_fuzz_settings(self): + secret, other, kwds = super(_bcrypt_test,self).get_fuzz_settings() + from passlib.handlers.bcrypt import IDENT_2, IDENT_2X + from passlib.utils import to_bytes + ident = kwds.get('ident') + if ident == IDENT_2X: + # 2x is just recognized, not supported. don't test with it. + del kwds['ident'] + elif ident == IDENT_2 and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret): + # avoid false failure due to flaw in 0-revision bcrypt: + # repeated strings like 'abc' and 'abcabc' hash identically. + other = self.get_fuzz_password() + return secret, other, kwds + + def fuzz_setting_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return randintgauss(5, 8, 6, 1) + + #=================================================================== + # custom tests + #=================================================================== + known_incorrect_padding = [ + # password, bad hash, good hash + + # 2 bits of salt padding set +# ("loppux", # \/ +# "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C", +# "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"), + ("test", # \/ + '$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO', + '$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'), + + # all 4 bits of salt padding set +# ("Passlib11", # \/ +# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK", +# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"), + ("test", # \/ + "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS", + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), + + # bad checksum padding + ("test", # \/ + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV", + "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"), + ] + + def test_90_bcrypt_padding(self): + "test passlib correctly handles bcrypt padding bits" + self.require_TEST_MODE("full") + # + # prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25) + # were some unused bits were incorrectly set in bcrypt salt strings. + # (fixed since 1.5.3) + # + bcrypt = self.handler + corr_desc = ".*incorrectly set padding bits" + + # + # test encrypt() / genconfig() don't generate invalid salts anymore + # + def check_padding(hash): + assert hash.startswith("$2a$") and len(hash) >= 28 + self.assertTrue(hash[28] in '.Oeu', + "unused bits incorrectly set in hash: %r" % (hash,)) + for i in irange(6): + check_padding(bcrypt.genconfig()) + for i in irange(3): + check_padding(bcrypt.encrypt("bob", rounds=bcrypt.min_rounds)) + + # + # test genconfig() corrects invalid salts & issues warning. + # + with self.assertWarningList(["salt too large", corr_desc]): + hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True) + self.assertEqual(hash, "$2a$05$" + "." * 22) + + # + # make sure genhash() corrects input + # + samples = self.known_incorrect_padding + for pwd, bad, good in samples: + with self.assertWarningList([corr_desc]): + self.assertEqual(bcrypt.genhash(pwd, bad), good) + with self.assertWarningList([]): + self.assertEqual(bcrypt.genhash(pwd, good), good) + + # + # and that verify() works good & bad + # + with self.assertWarningList([corr_desc]): + self.assertTrue(bcrypt.verify(pwd, bad)) + with self.assertWarningList([]): + self.assertTrue(bcrypt.verify(pwd, good)) + + # + # test normhash cleans things up correctly + # + for pwd, bad, good in samples: + with self.assertWarningList([corr_desc]): + self.assertEqual(bcrypt.normhash(bad), good) + with self.assertWarningList([]): + self.assertEqual(bcrypt.normhash(good), good) + self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc") + +hash.bcrypt._no_backends_msg() # call this for coverage purposes + +# create test cases for specific backends +bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_test, bcrypt_builtin_test = \ + _bcrypt_test.create_backend_cases(["pybcrypt", "bcryptor", "os_crypt", "builtin"]) + +#============================================================================= +# bigcrypt +#============================================================================= +class bigcrypt_test(HandlerCase): + handler = hash.bigcrypt + + # TODO: find an authoritative source of test vectors + known_correct_hashes = [ + + # + # various docs & messages on the web. + # + ("passphrase", "qiyh4XPJGsOZ2MEAyLkfWqeQ"), + ("This is very long passwd", "f8.SVpL2fvwjkAnxn8/rgTkwvrif6bjYB5c"), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, 'SEChBAyMbMNhgGLyP7kD1HZU'), + ] + + known_unidentified_hashes = [ + # one char short (10 % 11) + "qiyh4XPJGsOZ2MEAyLkfWqe" + + # one char too many (1 % 11) + "f8.SVpL2fvwjkAnxn8/rgTkwvrif6bjYB5cd" + ] + + # omit des_crypt from known_other since it's a valid bigcrypt hash too. + known_other_hashes = [row for row in HandlerCase.known_other_hashes + if row[0] != "des_crypt"] + + def test_90_internal(self): + # check that _norm_checksum() also validates checksum size. + # (current code uses regex in parser) + self.assertRaises(ValueError, hash.bigcrypt, use_defaults=True, + checksum=u('yh4XPJGsOZ')) + +#============================================================================= +# bsdi crypt +#============================================================================= +class _bsdi_crypt_test(HandlerCase): + "test BSDiCrypt algorithm" + handler = hash.bsdi_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '_J9..CCCCXBrJUJV154M'), + ('U*U***U', '_J9..CCCCXUhOBTXzaiE'), + ('U*U***U*', '_J9..CCCC4gQ.mB/PffM'), + ('*U*U*U*U', '_J9..XXXXvlzQGqpPPdk'), + ('*U*U*U*U*', '_J9..XXXXsqM/YSSP..Y'), + ('*U*U*U*U*U*U*U*U', '_J9..XXXXVL7qJCnku0I'), + ('*U*U*U*U*U*U*U*U*', '_J9..XXXXAj8cFbP5scI'), + ('ab1234567', '_J9..SDizh.vll5VED9g'), + ('cr1234567', '_J9..SDizRjWQ/zePPHc'), + ('zxyDPWgydbQjgq', '_J9..SDizxmRI1GjnQuE'), + ('726 even', '_K9..SaltNrQgIYUAeoY'), + ('', '_J9..SDSD5YGyRCr4W4c'), + + # + # custom + # + (" ", "_K1..crsmZxOLzfJH8iw"), + ("my", '_KR/.crsmykRplHbAvwA'), # <-- to detect old 12-bit rounds bug + ("my socra", "_K1..crsmf/9NzZr1fLM"), + ("my socrates", '_K1..crsmOv1rbde9A9o'), + ("my socrates note", "_K1..crsm/2qeAhdISMA"), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '_7C/.ABw0WIKy0ILVqo2'), + ] + known_unidentified_hashes = [ + # bad char in otherwise correctly formatted hash + # \/ + "_K1.!crsmZxOLzfJH8iw" + ] + + platform_crypt_support = [ + ("freebsd|openbsd|netbsd|darwin", True), + ("linux|solaris", False), + ] + + def setUp(self): + super(_bsdi_crypt_test, self).setUp() + warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd.*") + +bsdi_crypt_os_crypt_test, bsdi_crypt_builtin_test = \ + _bsdi_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +#============================================================================= +# cisco pix +#============================================================================= +class cisco_pix_test(UserHandlerMixin, HandlerCase): + handler = hash.cisco_pix + secret_size = 16 + requires_user = False + + known_correct_hashes = [ + # + # http://www.perlmonks.org/index.pl?node_id=797623 + # + ("cisco", "2KFQnbNIdI.2KYOU"), + + # + # http://www.hsc.fr/ressources/breves/pix_crack.html.en + # + ("hsc", "YtT8/k6Np8F1yz2c"), + + # + # www.freerainbowtables.com/phpBB3/viewtopic.php?f=2&t=1441 + # + ("", "8Ry2YjIyt7RRXU24"), + (("cisco", "john"), "hN7LzeyYjw12FSIU"), + (("cisco", "jack"), "7DrfeZ7cyOj/PslD"), + + # + # http://comments.gmane.org/gmane.comp.security.openwall.john.user/2529 + # + (("ripper", "alex"), "h3mJrcH0901pqX/m"), + (("cisco", "cisco"), "3USUcOPFUiMCO4Jk"), + (("cisco", "cisco1"), "3USUcOPFUiMCO4Jk"), + (("CscFw-ITC!", "admcom"), "lZt7HSIXw3.QP7.R"), + ("cangetin", "TynyB./ftknE77QP"), + (("cangetin", "rramsey"), "jgBZqYtsWfGcUKDi"), + + # + # http://openwall.info/wiki/john/sample-hashes + # + (("phonehome", "rharris"), "zyIIMSYjiPm0L7a6"), + + # + # from JTR 1.7.9 + # + ("test1", "TRPEas6f/aa6JSPL"), + ("test2", "OMT6mXmAvGyzrCtp"), + ("test3", "gTC7RIy1XJzagmLm"), + ("test4", "oWC1WRwqlBlbpf/O"), + ("password", "NuLKvvWGg.x9HEKO"), + ("0123456789abcdef", ".7nfVBEIEu4KbF/1"), + + # + # custom + # + (("cisco1", "cisco1"), "jmINXNH6p1BxUppp"), + + # ensures utf-8 used for unicode + (UPASS_TABLE, 'CaiIvkLMu2TOHXGT'), + ] + +#============================================================================= +# cisco type 7 +#============================================================================= +class cisco_type7_test(HandlerCase): + handler = hash.cisco_type7 + salt_bits = 4 + salt_type = int + + known_correct_hashes = [ + # + # http://mccltd.net/blog/?p=1034 + # + ("secure ", "04480E051A33490E"), + + # + # http://insecure.org/sploits/cisco.passwords.html + # + ("Its time to go to lunch!", + "153B1F1F443E22292D73212D5300194315591954465A0D0B59"), + + # + # http://blog.ioshints.info/2007/11/type-7-decryption-in-cisco-ios.html + # + ("t35t:pa55w0rd", "08351F1B1D431516475E1B54382F"), + + # + # http://www.m00nie.com/2011/09/cisco-type-7-password-decryption-and-encryption-with-perl/ + # + ("hiImTesting:)", "020E0D7206320A325847071E5F5E"), + + # + # http://packetlife.net/forums/thread/54/ + # + ("cisco123", "060506324F41584B56"), + ("cisco123", "1511021F07257A767B"), + + # + # source ? + # + ('Supe&8ZUbeRp4SS', "06351A3149085123301517391C501918"), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '0958EDC8A9F495F6F8A5FD'), + ] + + known_unidentified_hashes = [ + # salt with hex value + "0A480E051A33490E", + + # salt value > 52. this may in fact be valid, but we reject it for now + # (see docs for more). + '99400E4812', + ] + + def test_90_decode(self): + "test cisco_type7.decode()" + from passlib.utils import to_unicode, to_bytes + + handler = self.handler + for secret, hash in self.known_correct_hashes: + usecret = to_unicode(secret) + bsecret = to_bytes(secret) + self.assertEqual(handler.decode(hash), usecret) + self.assertEqual(handler.decode(hash, None), bsecret) + + self.assertRaises(UnicodeDecodeError, handler.decode, + '0958EDC8A9F495F6F8A5FD', 'ascii') + + def test_91_salt(self): + "test salt value border cases" + handler = self.handler + self.assertRaises(TypeError, handler, salt=None) + handler(salt=None, use_defaults=True) + self.assertRaises(TypeError, handler, salt='abc') + self.assertRaises(ValueError, handler, salt=-10) + with self.assertWarningList("salt/offset must be.*"): + h = handler(salt=100, relaxed=True) + self.assertEqual(h.salt, 52) + +#============================================================================= +# crypt16 +#============================================================================= +class crypt16_test(HandlerCase): + handler = hash.crypt16 + secret_size = 16 + + # TODO: find an authortative source of test vectors + known_correct_hashes = [ + # + # from messages around the web, including + # http://seclists.org/bugtraq/1999/Mar/76 + # + ("passphrase", "qi8H8R7OM4xMUNMPuRAZxlY."), + ("printf", "aaCjFz4Sh8Eg2QSqAReePlq6"), + ("printf", "AA/xje2RyeiSU0iBY3PDwjYo"), + ("LOLOAQICI82QB4IP", "/.FcK3mad6JwYt8LVmDqz9Lc"), + ("LOLOAQICI", "/.FcK3mad6JwYSaRHJoTPzY2"), + ("LOLOAQIC", "/.FcK3mad6JwYelhbtlysKy6"), + ("L", "/.CIu/PzYCkl6elhbtlysKy6"), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, 'YeDc9tKkkmDvwP7buzpwhoqQ'), + ] + +#============================================================================= +# des crypt +#============================================================================= +class _des_crypt_test(HandlerCase): + "test des-crypt algorithm" + handler = hash.des_crypt + secret_size = 8 + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', 'CCNf8Sbh3HDfQ'), + ('U*U***U', 'CCX.K.MFy4Ois'), + ('U*U***U*', 'CC4rMpbg9AMZ.'), + ('*U*U*U*U', 'XXxzOu6maQKqQ'), + ('', 'SDbsugeBiC58A'), + + # + # custom + # + ('', 'OgAwTx2l6NADI'), + (' ', '/Hk.VPuwQTXbc'), + ('test', 'N1tQbOFcM5fpg'), + ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'), + ('AlOtBsOl', 'cEpWz5IUCShqM'), + + # ensures utf-8 used for unicode + (u('hell\u00D6'), 'saykDgk3BPZ9E'), + ] + known_unidentified_hashes = [ + # bad char in otherwise correctly formatted hash + #\/ + '!gAwTx2l6NADI', + + # wrong size + 'OgAwTx2l6NAD', + 'OgAwTx2l6NADIj', + ] + + platform_crypt_support = [ + ("freebsd|openbsd|netbsd|linux|solaris|darwin", True), + ] + +des_crypt_os_crypt_test, des_crypt_builtin_test = \ + _des_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +#============================================================================= +# django +#============================================================================= +class _DjangoHelper(object): + # NOTE: not testing against Django < 1.0 since it doesn't support + # most of these hash formats. + + # flag if hash wasn't added until Django 1.4 + requires14 = False + + def fuzz_verifier_django(self): + from passlib.tests.test_ext_django import has_django1, has_django14 + if not has_django1: + return None + if self.requires14 and not has_django14: + return None + from django.contrib.auth.models import check_password + def verify_django(secret, hash): + "django/check_password" + if has_django14 and not secret: + return "skip" + if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): + hash = hash.replace("$$2y$", "$$2a$") + return check_password(secret, hash) + return verify_django + + def test_90_django_reference(self): + "run known correct hashes through Django's check_password()" + from passlib.tests.test_ext_django import has_django1, has_django14 + if self.requires14 and not has_django14: + raise self.skipTest("Django >= 1.4 not installed") + if not has_django1: + raise self.skipTest("Django >= 1.0 not installed") + from django.contrib.auth.models import check_password + assert self.known_correct_hashes + for secret, hash in self.iter_known_hashes(): + if has_django14 and not secret: + # django 1.4 rejects empty passwords + self.assertFalse(check_password(secret, hash), + "empty string should not have verified") + continue + self.assertTrue(check_password(secret, hash), + "secret=%r hash=%r failed to verify" % + (secret, hash)) + self.assertFalse(check_password('x' + secret, hash), + "mangled secret=%r hash=%r incorrect verified" % + (secret, hash)) + + def test_91_django_generation(self): + "test against output of Django's make_password()" + from passlib.tests.test_ext_django import has_django14 + if not has_django14: + raise self.skipTest("Django >= 1.4 not installed") + from passlib.utils import tick + from django.contrib.auth.hashers import make_password + name = self.handler.django_name # set for all the django_* handlers + end = tick() + self.max_fuzz_time/2 + while tick() < end: + secret, other = self.get_fuzz_password_pair() + if not secret: # django 1.4 rejects empty passwords. + continue + hash = make_password(secret, hasher=name) + self.assertTrue(self.do_identify(hash)) + self.assertTrue(self.do_verify(secret, hash)) + self.assertFalse(self.do_verify(other, hash)) + +class django_disabled_test(HandlerCase): + "test django_disabled" + handler = hash.django_disabled + is_disabled_handler = True + + known_correct_hashes = [ + # *everything* should hash to "!", and nothing should verify + ("password", "!"), + ("", "!"), + (UPASS_TABLE, "!"), + ] + +class django_des_crypt_test(HandlerCase, _DjangoHelper): + "test django_des_crypt" + handler = hash.django_des_crypt + secret_size = 8 + + known_correct_hashes = [ + # ensures only first two digits of salt count. + ("password", 'crypt$c2$c2M87q...WWcU'), + ("password", 'crypt$c2e86$c2M87q...WWcU'), + ("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'), + (UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'), + (u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"), + + # prevent regression of issue 22 + ("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'), + ] + + known_alternate_hashes = [ + # ensure django 1.4 empty salt field is accepted; + # but that salt field is re-filled (for django 1.0 compatibility) + ('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'), + ] + + known_unidentified_hashes = [ + 'sha1$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'crypt$c2$c2M87q', + + # salt must be >2 + 'crypt$f$c2M87q...WWcU', + + # make sure first 2 chars of salt & chk field agree. + 'crypt$ffe86$c2M87q...WWcU', + ] + +class django_salted_md5_test(HandlerCase, _DjangoHelper): + "test django_salted_md5" + handler = hash.django_salted_md5 + + known_correct_hashes = [ + # test extra large salt + ("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'), + + # test django 1.4 alphanumeric salt + ("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'), + (UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'), + ] + + known_unidentified_hashes = [ + 'sha1$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'md5$aa$bb', + ] + + def fuzz_setting_salt_size(self): + # workaround for django14 regression -- + # 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier. + # looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144 + # for now, we avoid salt_size==0 under 1.4 + handler = self.handler + from passlib.tests.test_ext_django import has_django14 + default = handler.default_salt_size + assert handler.min_salt_size == 0 + lower = 1 if has_django14 else 0 + upper = handler.max_salt_size or default*4 + return randintgauss(lower, upper, default, default*.5) + +class django_salted_sha1_test(HandlerCase, _DjangoHelper): + "test django_salted_sha1" + handler = hash.django_salted_sha1 + + known_correct_hashes = [ + # test extra large salt + ("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'), + + # test django 1.4 alphanumeric salt + ("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'), + + # ensures utf-8 used for unicode + (UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'), + (UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'), + + # generic password + ("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'), + ] + + known_unidentified_hashes = [ + 'md5$aa$bb', + ] + + known_malformed_hashes = [ + # checksum too short + 'sha1$c2e86$0f75', + ] + + fuzz_setting_salt_size = get_method_function(django_salted_md5_test.fuzz_setting_salt_size) + +class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper): + "test django_pbkdf2_sha256" + handler = hash.django_pbkdf2_sha256 + requires14 = True + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hasher + # + ('not a password', + 'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='), + (UPASS_TABLE, + 'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='), + ] + +class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper): + "test django_pbkdf2_sha1" + handler = hash.django_pbkdf2_sha1 + requires14 = True + + known_correct_hashes = [ + # + # custom - generated via django 1.4 hashers + # + ('not a password', + 'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='), + (UPASS_TABLE, + 'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='), + ] + +class django_bcrypt_test(HandlerCase, _DjangoHelper): + "test django_bcrypt" + handler = hash.django_bcrypt + secret_size = 72 + requires14 = True + + known_correct_hashes = [ + # + # just copied and adapted a few test vectors from bcrypt (above), + # since django_bcrypt is just a wrapper for the real bcrypt class. + # + ('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), + ('abcdefghijklmnopqrstuvwxyz', + 'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), + (UPASS_TABLE, + 'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), + ] + + # NOTE: the following have been cloned from _bcrypt_test() + + def populate_settings(self, kwds): + # speed up test w/ lower rounds + kwds.setdefault("rounds", 4) + super(django_bcrypt_test, self).populate_settings(kwds) + + def fuzz_setting_rounds(self): + # decrease default rounds for fuzz testing to speed up volume. + return randintgauss(5, 8, 6, 1) + + def fuzz_setting_ident(self): + # omit multi-ident tests, only $2a$ counts for this class + return None + +django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(), + "no bcrypt backends available")(django_bcrypt_test) + +#============================================================================= +# fshp +#============================================================================= +class fshp_test(HandlerCase): + "test fshp algorithm" + handler = hash.fshp + + known_correct_hashes = [ + # + # test vectors from FSHP reference implementation + # https://github.com/bdd/fshp-is-not-secure-anymore/blob/master/python/test.py + # + ('test', '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M='), + + ('test', + '{FSHP1|8|4096}MTIzNDU2NzjTdHcmoXwNc0f' + 'f9+ArUHoN0CvlbPZpxFi1C6RDM/MHSA==' + ), + + ('OrpheanBeholderScryDoubt', + '{FSHP1|8|4096}GVSUFDAjdh0vBosn1GUhz' + 'GLHP7BmkbCZVH/3TQqGIjADXpc+6NCg3g==' + ), + ('ExecuteOrder66', + '{FSHP3|16|8192}0aY7rZQ+/PR+Rd5/I9ss' + 'RM7cjguyT8ibypNaSp/U1uziNO3BVlg5qPU' + 'ng+zHUDQC3ao/JbzOnIBUtAeWHEy7a2vZeZ' + '7jAwyJJa2EqOsq4Io=' + ), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '{FSHP1|16|16384}9v6/l3Lu/d9by5nznpOS' + 'cqQo8eKu/b/CKli3RCkgYg4nRTgZu5y659YV8cCZ68UL'), + ] + + known_unidentified_hashes = [ + # incorrect header + '{FSHX0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + 'FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + ] + + known_malformed_hashes = [ + # bad base64 padding + '{FSHP0|0|1}qUqP5cyxm6YcTAhz05Hph5gvu9M', + + # wrong salt size + '{FSHP0|1|1}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + + # bad rounds + '{FSHP0|0|A}qUqP5cyxm6YcTAhz05Hph5gvu9M=', + ] + + def test_90_variant(self): + "test variant keyword" + handler = self.handler + kwds = dict(salt=b('a'), rounds=1) + + # accepts ints + handler(variant=1, **kwds) + + # accepts bytes or unicode + handler(variant=u('1'), **kwds) + handler(variant=b('1'), **kwds) + + # aliases + handler(variant=u('sha256'), **kwds) + handler(variant=b('sha256'), **kwds) + + # rejects None + self.assertRaises(TypeError, handler, variant=None, **kwds) + + # rejects other types + self.assertRaises(TypeError, handler, variant=complex(1,1), **kwds) + + # invalid variant + self.assertRaises(ValueError, handler, variant='9', **kwds) + self.assertRaises(ValueError, handler, variant=9, **kwds) + +#============================================================================= +# hex digests +#============================================================================= +class hex_md4_test(HandlerCase): + handler = hash.hex_md4 + known_correct_hashes = [ + ("password", '8a9d093f14f8701df17732b2bb182c74'), + (UPASS_TABLE, '876078368c47817ce5f9115f3a42cf74'), + ] + +class hex_md5_test(HandlerCase): + handler = hash.hex_md5 + known_correct_hashes = [ + ("password", '5f4dcc3b5aa765d61d8327deb882cf99'), + (UPASS_TABLE, '05473f8a19f66815e737b33264a0d0b0'), + ] + +class hex_sha1_test(HandlerCase): + handler = hash.hex_sha1 + known_correct_hashes = [ + ("password", '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'), + (UPASS_TABLE, 'e059b2628e3a3e2de095679de9822c1d1466e0f0'), + ] + +class hex_sha256_test(HandlerCase): + handler = hash.hex_sha256 + known_correct_hashes = [ + ("password", '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'), + (UPASS_TABLE, '6ed729e19bf24d3d20f564375820819932029df05547116cfc2cc868a27b4493'), + ] + +class hex_sha512_test(HandlerCase): + handler = hash.hex_sha512 + known_correct_hashes = [ + ("password", 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c' + '706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cac' + 'bc86'), + (UPASS_TABLE, 'd91bb0a23d66dca07a1781fd63ae6a05f6919ee5fc368049f350c9f' + '293b078a18165d66097cf0d89fdfbeed1ad6e7dba2344e57348cd6d51308c843a06f' + '29caf'), + ] + +#============================================================================= +# htdigest hash +#============================================================================= +class htdigest_test(UserHandlerMixin, HandlerCase): + handler = hash.htdigest + + known_correct_hashes = [ + # secret, user, realm + + # from RFC 2617 + (("Circle Of Life", "Mufasa", "testrealm@host.com"), + '939e7578ed9e3c518a452acee763bce9'), + + # custom + ((UPASS_TABLE, UPASS_USD, UPASS_WAV), + '4dabed2727d583178777fab468dd1f17'), + ] + + known_unidentified_hashes = [ + # bad char \/ - currently rejecting upper hex chars, may change + '939e7578edAe3c518a452acee763bce9', + + # bad char \/ + '939e7578edxe3c518a452acee763bce9', + ] + + def test_80_user(self): + raise self.skipTest("test case doesn't support 'realm' keyword") + + def populate_context(self, secret, kwds): + "insert username into kwds" + if isinstance(secret, tuple): + secret, user, realm = secret + else: + user, realm = "user", "realm" + kwds.setdefault("user", user) + kwds.setdefault("realm", realm) + return secret + +#============================================================================= +# ldap hashes +#============================================================================= +class ldap_md5_test(HandlerCase): + handler = hash.ldap_md5 + known_correct_hashes = [ + ("helloworld", '{MD5}/F4DjTilcDIIVEHn/nAQsA=='), + (UPASS_TABLE, '{MD5}BUc/ihn2aBXnN7MyZKDQsA=='), + ] + +class ldap_sha1_test(HandlerCase): + handler = hash.ldap_sha1 + known_correct_hashes = [ + ("helloworld", '{SHA}at+xg6SiyUovktq1redipHiJpaE='), + (UPASS_TABLE, '{SHA}4FmyYo46Pi3glWed6YIsHRRm4PA='), + ] + +class ldap_salted_md5_test(HandlerCase): + handler = hash.ldap_salted_md5 + known_correct_hashes = [ + ("testing1234", '{SMD5}UjFY34os/pnZQ3oQOzjqGu4yeXE='), + (UPASS_TABLE, '{SMD5}Z0ioJ58LlzUeRxm3K6JPGAvBGIM='), + + # alternate salt sizes (8, 15, 16) + ('test', '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw'), + ('test', '{SMD5}XRlncfRzvGi0FDzgR98tUgBg7B3jXOs9p9S615qTkg=='), + ('test', '{SMD5}FbAkzOMOxRbMp6Nn4hnZuel9j9Gas7a2lvI+x5hT6j0='), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SMD5}IGVhwK+anvspmfDt2t0vgGjt/Q==', + + # incorrect base64 encoding + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4c', + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw' + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw=', + '{SMD5}LnuZPJhiaY95/4lmV=pg548xBsD4P4cw', + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P===', + ] + +class ldap_salted_sha1_test(HandlerCase): + handler = hash.ldap_salted_sha1 + known_correct_hashes = [ + ("testing123", '{SSHA}0c0blFTXXNuAMHECS4uxrj3ZieMoWImr'), + ("secret", "{SSHA}0H+zTv8o4MR4H43n03eCsvw1luG8LdB7"), + (UPASS_TABLE, '{SSHA}3yCSD1nLZXznra4N8XzZgAL+s1sQYsx5'), + + # alternate salt sizes (8, 15, 16) + ('test', '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOckw=='), + ('test', '{SSHA}/ZMF5KymNM+uEOjW+9STKlfCFj51bg3BmBNCiPHeW2ttbU0='), + ('test', '{SSHA}Pfx6Vf48AT9x3FVv8znbo8WQkEVSipHSWovxXmvNWUvp/d/7'), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA}ZQK3Yvtvl6wtIRoISgMGPkcWU7Nfq5U=', + + # incorrect base64 encoding + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck', + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOckw=', + '{SSHA}P90+qijSp8MJ1tN25j5o1Pf=UvlqjXHOGeOckw==', + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck===', + ] + +class ldap_plaintext_test(HandlerCase): + # TODO: integrate EncodingHandlerMixin + handler = hash.ldap_plaintext + known_correct_hashes = [ + ("password", 'password'), + (UPASS_TABLE, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + (PASS_TABLE_UTF8, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + ] + known_unidentified_hashes = [ + "{FOO}bar", + + # NOTE: this hash currently rejects the empty string. + "", + ] + + known_other_hashes = [ + ("ldap_md5", "{MD5}/F4DjTilcDIIVEHn/nAQsA==") + ] + + def get_fuzz_password(self): + # NOTE: this hash currently rejects the empty string. + while True: + pwd = super(ldap_plaintext_test, self).get_fuzz_password() + if pwd: + return pwd + +class _ldap_md5_crypt_test(HandlerCase): + # NOTE: since the ldap_{crypt} handlers are all wrappers, don't need + # separate test; this is just to test the codebase end-to-end + handler = hash.ldap_md5_crypt + + known_correct_hashes = [ + # + # custom + # + ('', '{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), + (' ', '{CRYPT}$1$m/5ee7ol$bZn0kIBFipq39e.KDXX8I0'), + ('test', '{CRYPT}$1$ec6XvcoW$ghEtNK2U1MC5l.Dwgi3020'), + ('Compl3X AlphaNu3meric', '{CRYPT}$1$nX1e7EeI$ljQn72ZUgt6Wxd9hfvHdV0'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '{CRYPT}$1$jQS7o98J$V6iTcr71CGgwW2laf17pi1'), + ('test', '{CRYPT}$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '{CRYPT}$1$d6/Ky1lU$/xpf8m7ftmWLF.TjHCqel0'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + '{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', + ] + +ldap_md5_crypt_os_crypt_test, ldap_md5_crypt_builtin_test = \ + _ldap_md5_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +class _ldap_sha1_crypt_test(HandlerCase): + # NOTE: this isn't for testing the hash (see ldap_md5_crypt note) + # but as a self-test of the os_crypt patching code in HandlerCase. + handler = hash.ldap_sha1_crypt + + known_correct_hashes = [ + ('password', '{CRYPT}$sha1$10$c.mcTzCw$gF8UeYst9yXX7WNZKc5Fjkq0.au7'), + (UPASS_TABLE, '{CRYPT}$sha1$10$rnqXlOsF$aGJf.cdRPewJAXo1Rn1BkbaYh0fP'), + ] + + def populate_settings(self, kwds): + kwds.setdefault("rounds", 10) + super(_ldap_sha1_crypt_test, self).populate_settings(kwds) + + def test_77_fuzz_input(self): + raise self.skipTest("unneeded") + +ldap_sha1_crypt_os_crypt_test, = _ldap_sha1_crypt_test.create_backend_cases(["os_crypt"]) + +#============================================================================= +# ldap_pbkdf2_{digest} +#============================================================================= +# NOTE: since these are all wrappers for the pbkdf2_{digest} hasehs, +# they don't extensive separate testing. + +class ldap_pbkdf2_test(TestCase): + + def test_wrappers(self): + "test ldap pbkdf2 wrappers" + + self.assertTrue( + hash.ldap_pbkdf2_sha1.verify( + "password", + '{PBKDF2}1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI', + ) + ) + + self.assertTrue( + hash.ldap_pbkdf2_sha256.verify( + "password", + '{PBKDF2-SHA256}1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg' + '.fJPeq1h/gXXY7acBp9/6c.tmQ' + ) + ) + + self.assertTrue( + hash.ldap_pbkdf2_sha512.verify( + "password", + '{PBKDF2-SHA512}1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' + '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' + ) + ) + +#============================================================================= +# lanman +#============================================================================= +class lmhash_test(EncodingHandlerMixin, HandlerCase): + handler = hash.lmhash + secret_size = 14 + secret_case_insensitive = True + + known_correct_hashes = [ + # + # http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx + # + ("OLDPASSWORD", "c9b81d939d6fd80cd408e6b105741864"), + ("NEWPASSWORD", '09eeab5aa415d6e4d408e6b105741864'), + ("welcome", "c23413a8a1e7665faad3b435b51404ee"), + + # + # custom + # + ('', 'aad3b435b51404eeaad3b435b51404ee'), + ('zzZZZzz', 'a5e6066de61c3e35aad3b435b51404ee'), + ('passphrase', '855c3697d9979e78ac404c4ba2c66533'), + ('Yokohama', '5ecd9236d21095ce7584248b8d2c9f9e'), + + # ensures cp437 used for unicode + (u('ENCYCLOP\xC6DIA'), 'fed6416bffc9750d48462b9d7aaac065'), + (u('encyclop\xE6dia'), 'fed6416bffc9750d48462b9d7aaac065'), + + # test various encoding values + ((u("\xC6"), None), '25d8ab4a0659c97aaad3b435b51404ee'), + ((u("\xC6"), "cp437"), '25d8ab4a0659c97aaad3b435b51404ee'), + ((u("\xC6"), "latin-1"), '184eecbbe9991b44aad3b435b51404ee'), + ((u("\xC6"), "utf-8"), '00dd240fcfab20b8aad3b435b51404ee'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '855c3697d9979e78ac404c4ba2c6653X', + ] + + def test_90_raw(self): + "test lmhash.raw() method" + from binascii import unhexlify + from passlib.utils.compat import str_to_bascii + lmhash = self.handler + for secret, hash in self.known_correct_hashes: + kwds = {} + secret = self.populate_context(secret, kwds) + data = unhexlify(str_to_bascii(hash)) + self.assertEqual(lmhash.raw(secret, **kwds), data) + self.assertRaises(TypeError, lmhash.raw, 1) + +#============================================================================= +# md5 crypt +#============================================================================= +class _md5_crypt_test(HandlerCase): + handler = hash.md5_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$1$dXc3I7Rw$ctlgjDdWJLMT.qwHsWhXR1'), + ('U*U***U', '$1$dXc3I7Rw$94JPyQc/eAgQ3MFMCoMF.0'), + ('U*U***U*', '$1$dXc3I7Rw$is1mVIAEtAhIzSdfn5JOO0'), + ('*U*U*U*U', '$1$eQT9Hwbt$XtuElNJD.eW5MN5UCWyTQ0'), + ('', '$1$Eu.GHtia$CFkL/nE1BYTlEPiVx1VWX0'), + + # + # custom + # + + # NOTE: would need to patch HandlerCase to coerce hashes + # to native str for this first one to work under py3. +## ('', b('$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.')), + ('', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), + (' ', '$1$m/5ee7ol$bZn0kIBFipq39e.KDXX8I0'), + ('test', '$1$ec6XvcoW$ghEtNK2U1MC5l.Dwgi3020'), + ('Compl3X AlphaNu3meric', '$1$nX1e7EeI$ljQn72ZUgt6Wxd9hfvHdV0'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$1$jQS7o98J$V6iTcr71CGgwW2laf17pi1'), + ('test', '$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), + (b('test'), '$1$SuMrG47N$ymvzYjr7QcEQjaK5m1PGx1'), + (u('s'), '$1$ssssssss$YgmLTApYTv12qgTwBoj8i/'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$1$d6/Ky1lU$/xpf8m7ftmWLF.TjHCqel0'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash \/ + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', + + # too many fields + '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.$', + ] + + platform_crypt_support = [ + ("freebsd|openbsd|netbsd|linux|solaris", True), + ("darwin", False), + ] + +md5_crypt_os_crypt_test, md5_crypt_builtin_test = \ + _md5_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +#============================================================================= +# msdcc 1 & 2 +#============================================================================= +class msdcc_test(UserHandlerMixin, HandlerCase): + handler = hash.msdcc + user_case_insensitive = True + + known_correct_hashes = [ + + # + # http://www.jedge.com/wordpress/windows-password-cache/ + # + (("Asdf999", "sevans"), "b1176c2587478785ec1037e5abc916d0"), + + # + # http://infosecisland.com/blogview/12156-Cachedump-for-Meterpreter-in-Action.html + # + (("ASDqwe123", "jdoe"), "592cdfbc3f1ef77ae95c75f851e37166"), + + # + # http://comments.gmane.org/gmane.comp.security.openwall.john.user/1917 + # + (("test1", "test1"), "64cd29e36a8431a2b111378564a10631"), + (("test2", "test2"), "ab60bdb4493822b175486810ac2abe63"), + (("test3", "test3"), "14dd041848e12fc48c0aa7a416a4a00c"), + (("test4", "test4"), "b945d24866af4b01a6d89b9d932a153c"), + + # + # http://ciscoit.wordpress.com/2011/04/13/metasploit-hashdump-vs-cachedump/ + # + (("1234qwer!@#$", "Administrator"), "7b69d06ef494621e3f47b9802fe7776d"), + + # + # http://www.securiteam.com/tools/5JP0I2KFPA.html + # + (("password", "user"), "2d9f0b052932ad18b87f315641921cda"), + + # + # from JTR 1.7.9 + # + (("", "root"), "176a4c2bd45ac73687676c2f09045353"), + (("test1", "TEST1"), "64cd29e36a8431a2b111378564a10631"), + (("okolada", "nineteen_characters"), "290efa10307e36a79b3eebf2a6b29455"), + ((u("\u00FC"), u("\u00FC")), "48f84e6f73d6d5305f6558a33fa2c9bb"), + ((u("\u00FC\u00FC"), u("\u00FC\u00FC")), "593246a8335cf0261799bda2a2a9c623"), + ((u("\u20AC\u20AC"), "user"), "9121790702dda0fa5d353014c334c2ce"), + + # + # custom + # + + # ensures utf-8 used for unicode + ((UPASS_TABLE, 'bob'), 'fcb82eb4212865c7ac3503156ca3f349'), + ] + + known_alternate_hashes = [ + # check uppercase accepted. + ("B1176C2587478785EC1037E5ABC916D0", ("Asdf999", "sevans"), + "b1176c2587478785ec1037e5abc916d0"), + ] + +class msdcc2_test(UserHandlerMixin, HandlerCase): + handler = hash.msdcc2 + user_case_insensitive = True + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + (("test1", "test1"), "607bbe89611e37446e736f7856515bf8"), + (("qerwt", "Joe"), "e09b38f84ab0be586b730baf61781e30"), + (("12345", "Joe"), "6432f517a900b3fc34ffe57f0f346e16"), + (("", "bin"), "c0cbe0313a861062e29f92ede58f9b36"), + (("w00t", "nineteen_characters"), "87136ae0a18b2dafe4a41d555425b2ed"), + (("w00t", "eighteencharacters"), "fc5df74eca97afd7cd5abb0032496223"), + (("longpassword", "twentyXXX_characters"), "cfc6a1e33eb36c3d4f84e4c2606623d2"), + (("longpassword", "twentyoneX_characters"), "99ff74cea552799da8769d30b2684bee"), + (("longpassword", "twentytwoXX_characters"), "0a721bdc92f27d7fb23b87a445ec562f"), + (("test2", "TEST2"), "c6758e5be7fc943d00b97972a8a97620"), + (("test3", "test3"), "360e51304a2d383ea33467ab0b639cc4"), + (("test4", "test4"), "6f79ee93518306f071c47185998566ae"), + ((u("\u00FC"), "joe"), "bdb80f2c4656a8b8591bd27d39064a54"), + ((u("\u20AC\u20AC"), "joe"), "1e1e20f482ff748038e47d801d0d1bda"), + ((u("\u00FC\u00FC"), "admin"), "0839e4a07c00f18a8c65cf5b985b9e73"), + + # + # custom + # + + # custom unicode test + ((UPASS_TABLE, 'bob'), 'cad511dc9edefcf69201da72efb6bb55'), + ] + +#============================================================================= +# mssql 2000 & 2005 +#============================================================================= +class mssql2000_test(HandlerCase): + handler = hash.mssql2000 + secret_case_insensitive = "verify-only" + # FIXME: fix UT framework - this hash is sensitive to password case, but verify() is not + + known_correct_hashes = [ + # + # http://hkashfi.blogspot.com/2007/08/breaking-sql-server-2005-hashes.html + # + ('Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED2503412FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + ('TEST', '0x010034767D5C2FD54D6119FFF04129A1D72E7C3194F7284A7F3A2FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + + # + # http://www.sqlmag.com/forums/aft/68438 + # + ('x', '0x010086489146C46DD7318D2514D1AC706457CBF6CD3DF8407F071DB4BBC213939D484BF7A766E974F03C96524794'), + + # + # http://stackoverflow.com/questions/173329/how-to-decrypt-a-password-from-sql-server + # + ('AAAA', '0x0100CF465B7B12625EF019E157120D58DD46569AC7BF4118455D12625EF019E157120D58DD46569AC7BF4118455D'), + + # + # http://msmvps.com/blogs/gladchenko/archive/2005/04/06/41083.aspx + # + ('123', '0x01002D60BA07FE612C8DE537DF3BFCFA49CD9968324481C1A8A8FE612C8DE537DF3BFCFA49CD9968324481C1A8A8'), + + # + # http://www.simple-talk.com/sql/t-sql-programming/temporarily-changing-an-unknown-password-of-the-sa-account-/ + # + ('12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + + # + # XXX: sample is incomplete, password unknown + # https://anthonystechblog.wordpress.com/2011/04/20/password-encryption-in-sql-server-how-to-tell-if-a-user-is-using-a-weak-password/ + # (????, '0x0100813F782D66EF15E40B1A3FDF7AB88B322F51401A87D8D3E3A8483C4351A3D96FC38499E6CDD2B6F?????????'), + # + + # + # from JTR 1.7.9 + # + ('foo', '0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8B6D6261460D3F53B279CC6913CE747006A2E3254'), + ('bar', '0x01000508513EADDF6DB7DDD270CCA288BF097F2FF69CC2DB74FBB9644D6901764F999BAB9ECB80DE578D92E3F80D'), + ('canard', '0x01008408C523CF06DCB237835D701C165E68F9460580132E28ED8BC558D22CEDF8801F4503468A80F9C52A12C0A3'), + ('lapin', '0x0100BF088517935FC9183FE39FDEC77539FD5CB52BA5F5761881E5B9638641A79DBF0F1501647EC941F3355440A2'), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_USD, '0x0100624C0961B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5'), + (UPASS_TABLE, '0x010083104228FAD559BE52477F2131E538BE9734E5C4B0ADEFD7F6D784B03C98585DC634FE2B8CA3A6DFFEC729B4'), + + ] + + known_correct_configs = [ + ('0x010034767D5C00000000000000000000000000000000000000000000000000000000000000000000000000000000', + 'Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED2503412FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + ] + + known_alternate_hashes = [ + # lower case hex + ('0x01005b20054332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b3', + '12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + ] + + known_unidentified_hashes = [ + # malformed start + '0X01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + + # wrong magic value + '0x02005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + + # wrong size + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3', + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3AF', + + # mssql2005 + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + ] + + known_malformed_hashes = [ + # non-hex char -----\/ + b('0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + u('0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + ] + +class mssql2005_test(HandlerCase): + handler = hash.mssql2005 + + known_correct_hashes = [ + # + # http://hkashfi.blogspot.com/2007/08/breaking-sql-server-2005-hashes.html + # + ('TEST', '0x010034767D5C2FD54D6119FFF04129A1D72E7C3194F7284A7F3A'), + + # + # http://www.openwall.com/lists/john-users/2009/07/14/2 + # + ('toto', '0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908'), + + # + # http://msmvps.com/blogs/gladchenko/archive/2005/04/06/41083.aspx + # + ('123', '0x01004A335DCEDB366D99F564D460B1965B146D6184E4E1025195'), + ('123', '0x0100E11D573F359629B344990DCD3D53DE82CF8AD6BBA7B638B6'), + + # + # XXX: password unknown + # http://www.simple-talk.com/sql/t-sql-programming/temporarily-changing-an-unknown-password-of-the-sa-account-/ + # (???, '0x01004086CEB6301EEC0A994E49E30DA235880057410264030797'), + # + + # + # http://therelentlessfrontend.com/2010/03/26/encrypting-and-decrypting-passwords-in-sql-server/ + # + ('AAAA', '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30'), + + # + # from JTR 1.7.9 + # + ("toto", "0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908"), + ("titi", "0x01004086CEB60ED526885801C23B366965586A43D3DEAC6DD3FD"), + ("foo", "0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8"), + ("bar", "0x01000508513EADDF6DB7DDD270CCA288BF097F2FF69CC2DB74FB"), + ("canard", "0x01008408C523CF06DCB237835D701C165E68F9460580132E28ED"), + ("lapin", "0x0100BF088517935FC9183FE39FDEC77539FD5CB52BA5F5761881"), + + # + # adapted from mssql2000.known_correct_hashes (above) + # + ('Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED250341'), + ('Test', '0x0100993BF2315F36CC441485B35C4D84687DC02C78B0E680411F'), + ('x', '0x010086489146C46DD7318D2514D1AC706457CBF6CD3DF8407F07'), + ('AAAA', '0x0100CF465B7B12625EF019E157120D58DD46569AC7BF4118455D'), + ('123', '0x01002D60BA07FE612C8DE537DF3BFCFA49CD9968324481C1A8A8'), + ('12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_USD, '0x0100624C0961B28E39FEE13FD0C35F57B4523F0DA1861C11D5A5'), + (UPASS_TABLE, '0x010083104228FAD559BE52477F2131E538BE9734E5C4B0ADEFD7'), + ] + + known_correct_configs = [ + ('0x010034767D5C0000000000000000000000000000000000000000', + 'Test', '0x010034767D5C0CFA5FDCA28C4A56085E65E882E71CB0ED250341'), + ] + + known_alternate_hashes = [ + # lower case hex + ('0x01005b20054332752e1bc2e7c5df0f9ebfe486e9bee063e8d3b3', + '12345', '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'), + ] + + known_unidentified_hashes = [ + # malformed start + '0X010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30', + + # wrong magic value + '0x020036D726AE86834E97F20B198ACD219D60B446AC5E48C54F30', + + # wrong size + '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F', + '0x010036D726AE86834E97F20B198ACD219D60B446AC5E48C54F3012', + + # mssql2000 + '0x01005B20054332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3', + ] + + known_malformed_hashes = [ + # non-hex char --\/ + '0x010036D726AE86G34E97F20B198ACD219D60B446AC5E48C54F30', + ] + +#============================================================================= +# mysql 323 & 41 +#============================================================================= +class mysql323_test(HandlerCase): + handler = hash.mysql323 + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('drew', '697a7de87c5390b2'), + ('password', "5d2e19393cc5ef67"), + + # + # custom + # + ('mypass', '6f8c114b58f2ce9e'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '4ef327ca5491c8d7'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '6z8c114b58f2ce9e', + ] + + def test_90_whitespace(self): + "check whitespace is ignored per spec" + h = self.do_encrypt("mypass") + h2 = self.do_encrypt("my pass") + self.assertEqual(h, h2) + + def accept_fuzz_pair(self, secret, other): + # override to handle whitespace + return secret.replace(" ","") != other.replace(" ","") + +class mysql41_test(HandlerCase): + handler = hash.mysql41 + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('verysecretpassword', '*2C905879F74F28F8570989947D06A8429FB943E6'), + ('12345678123456781234567812345678', '*F9F1470004E888963FB466A5452C9CBD9DF6239C'), + ("' OR 1 /*'", '*97CF7A3ACBE0CA58D5391AC8377B5D9AC11D46D9'), + + # + # custom + # + ('mypass', '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '*E7AFE21A9CFA2FC9D15D942AE8FB5C240FE5837B'), + ] + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '*6Z8989366EAF75BB670AD8EA7A7FC1176A95CEF4', + ] + +#============================================================================= +# NTHASH +#============================================================================= +class nthash_test(HandlerCase): + handler = hash.nthash + + known_correct_hashes = [ + # + # http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx + # + ("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")), + ("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")), + + # + # from JTR 1.7.9 + # + + # ascii + ('', '31d6cfe0d16ae931b73c59d7e0c089c0'), + ('tigger', 'b7e0ea9fbffcf6dd83086e905089effd'), + + # utf-8 + (b('\xC3\xBC'), '8bd6e4fb88e01009818749c5443ea712'), + (b('\xC3\xBC\xC3\xBC'), 'cc1260adb6985ca749f150c7e0b22063'), + (b('\xE2\x82\xAC'), '030926b781938db4365d46adc7cfbcb8'), + (b('\xE2\x82\xAC\xE2\x82\xAC'),'682467b963bb4e61943e170a04f7db46'), + + # + # custom + # + ('passphrase', '7f8fe03093cc84b267b109625f6bbf4b'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash + '7f8fe03093cc84b267b109625f6bbfxb', + ] + +class bsd_nthash_test(HandlerCase): + handler = hash.bsd_nthash + + known_correct_hashes = [ + ('passphrase', '$3$$7f8fe03093cc84b267b109625f6bbf4b'), + (b('\xC3\xBC'), '$3$$8bd6e4fb88e01009818749c5443ea712'), + ] + + known_unidentified_hashes = [ + # bad char in otherwise correct hash --\/ + '$3$$7f8fe03093cc84b267b109625f6bbfxb', + ] + +#============================================================================= +# oracle 10 & 11 +#============================================================================= +class oracle10_test(UserHandlerMixin, HandlerCase): + handler = hash.oracle10 + secret_case_insensitive = True + user_case_insensitive = True + + # TODO: get more test vectors (especially ones which properly test unicode) + known_correct_hashes = [ + # ((secret,user),hash) + + # + # http://www.petefinnigan.com/default/default_password_list.htm + # + (('tiger', 'scott'), 'F894844C34402B67'), + ((u('ttTiGGeR'), u('ScO')), '7AA1A84E31ED7771'), + (("d_syspw", "SYSTEM"), '1B9F1F9A5CB9EB31'), + (("strat_passwd", "strat_user"), 'AEBEDBB4EFB5225B'), + + # + # http://openwall.info/wiki/john/sample-hashes + # + (('#95LWEIGHTS', 'USER'), '000EA4D72A142E29'), + (('CIAO2010', 'ALFREDO'), 'EB026A76F0650F7B'), + + # + # from JTR 1.7.9 + # + (('GLOUGlou', 'Bob'), 'CDC6B483874B875B'), + (('GLOUGLOUTER', 'bOB'), 'EF1F9139DB2D5279'), + (('LONG_MOT_DE_PASSE_OUI', 'BOB'), 'EC8147ABB3373D53'), + + # + # custom + # + ((UPASS_TABLE, 'System'), 'B915A853F297B281'), + ] + + known_unidentified_hashes = [ + # bad char in hash --\ + 'F894844C34402B6Z', + ] + +class oracle11_test(HandlerCase): + handler = hash.oracle11 + # TODO: find more test vectors (especially ones which properly test unicode) + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ("abc123", "S:5FDAB69F543563582BA57894FE1C1361FB8ED57B903603F2C52ED1B4D642"), + ("SyStEm123!@#", "S:450F957ECBE075D2FA009BA822A9E28709FBC3DA82B44D284DDABEC14C42"), + ("oracle", "S:3437FF72BD69E3FB4D10C750B92B8FB90B155E26227B9AB62D94F54E5951"), + ("11g", "S:61CE616647A4F7980AFD7C7245261AF25E0AFE9C9763FCF0D54DA667D4E6"), + ("11g", "S:B9E7556F53500C8C78A58F50F24439D79962DE68117654B6700CE7CC71CF"), + + # + # source? + # + ("SHAlala", "S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C"), + + # + # custom + # + (UPASS_TABLE, 'S:51586343E429A6DF024B8F242F2E9F8507B1096FACD422E29142AA4974B0'), + ] + +#============================================================================= +# pbkdf2 hashes +#============================================================================= +class atlassian_pbkdf2_sha1_test(HandlerCase): + handler = hash.atlassian_pbkdf2_sha1 + + known_correct_hashes = [ + # + # generated using Jira + # + ("admin", '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/p'), + (UPASS_WAV, + "{PKCS5S2}cE9Yq6Am5tQGdHSHhky2XLeOnURwzaLBG2sur7FHKpvy2u0qDn6GcVGRjlmJoIUy"), + ] + + known_malformed_hashes = [ + # bad char ---\/ + '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy!0IPksHChwoTAVYFrhsgoq8/p' + + # bad size, missing padding + '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/' + + # bad size, with correct padding + '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/=' + ] + +class pbkdf2_sha1_test(HandlerCase): + handler = hash.pbkdf2_sha1 + known_correct_hashes = [ + ("password", '$pbkdf2$1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI'), + (UPASS_WAV, + '$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc'), + ] + + known_malformed_hashes = [ + # zero padded rounds field + '$pbkdf2$01212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc', + + # empty rounds field + '$pbkdf2$$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc', + + # too many field + '$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc$', + ] + +class pbkdf2_sha256_test(HandlerCase): + handler = hash.pbkdf2_sha256 + known_correct_hashes = [ + ("password", + '$pbkdf2-sha256$1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg.fJPeq1h/gXXY7acBp9/6c.tmQ' + ), + (UPASS_WAV, + '$pbkdf2-sha256$1212$3SABFJGDtyhrQMVt1uABPw$WyaUoqCLgvz97s523nF4iuOqZNbp5Nt8do/cuaa7AiI' + ), + ] + +class pbkdf2_sha512_test(HandlerCase): + handler = hash.pbkdf2_sha512 + known_correct_hashes = [ + ("password", + '$pbkdf2-sha512$1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' + '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' + ), + (UPASS_WAV, + '$pbkdf2-sha512$1212$KkbvoKGsAIcF8IslDR6skQ$8be/PRmd88Ps8fmPowCJt' + 'tH9G3vgxpG.Krjt3KT.NP6cKJ0V4Prarqf.HBwz0dCkJ6xgWnSj2ynXSV7MlvMa8Q' + ), + ] + +class cta_pbkdf2_sha1_test(HandlerCase): + handler = hash.cta_pbkdf2_sha1 + known_correct_hashes = [ + # + # test vectors from original implementation + # + (u("hashy the \N{SNOWMAN}"), '$p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0='), + + # + # custom + # + ("password", "$p5k2$1$$h1TDLGSw9ST8UMAPeIE13i0t12c="), + (UPASS_WAV, + "$p5k2$4321$OTg3NjU0MzIx$jINJrSvZ3LXeIbUdrJkRpN62_WQ="), + ] + +class dlitz_pbkdf2_sha1_test(HandlerCase): + handler = hash.dlitz_pbkdf2_sha1 + known_correct_hashes = [ + # + # test vectors from original implementation + # + ('cloadm', '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'), + ('gnu', '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'), + ('dcl', '$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL'), + ('spam', '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'), + (UPASS_WAV, + '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'), + ] + +class grub_pbkdf2_sha512_test(HandlerCase): + handler = hash.grub_pbkdf2_sha512 + known_correct_hashes = [ + # + # test vectors generated from cmd line tool + # + + # salt=32 bytes + (UPASS_WAV, + 'grub.pbkdf2.sha512.10000.BCAC1CEC5E4341C8C511C529' + '7FA877BE91C2817B32A35A3ECF5CA6B8B257F751.6968526A' + '2A5B1AEEE0A29A9E057336B48D388FFB3F600233237223C21' + '04DE1752CEC35B0DD1ED49563398A282C0F471099C2803FBA' + '47C7919CABC43192C68F60'), + + # salt=64 bytes + ('toomanysecrets', + 'grub.pbkdf2.sha512.10000.9B436BB6978682363D5C449B' + 'BEAB322676946C632208BC1294D51F47174A9A3B04A7E4785' + '986CD4EA7470FAB8FE9F6BD522D1FC6C51109A8596FB7AD48' + '7C4493.0FE5EF169AFFCB67D86E2581B1E251D88C777B98BA' + '2D3256ECC9F765D84956FC5CA5C4B6FD711AA285F0A04DCF4' + '634083F9A20F4B6F339A52FBD6BED618E527B'), + + ] + +#============================================================================= +# PHPass Portable Crypt +#============================================================================= +class phpass_test(HandlerCase): + handler = hash.phpass + + known_correct_hashes = [ + # + # from official 0.3 implementation + # http://www.openwall.com/phpass/ + # + ('test12345', '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0'), # from the source + + # + # from JTR 1.7.9 + # + ('test1', '$H$9aaaaaSXBjgypwqm.JsMssPLiS8YQ00'), + ('123456', '$H$9PE8jEklgZhgLmZl5.HYJAzfGCQtzi1'), + ('123456', '$H$9pdx7dbOW3Nnt32sikrjAxYFjX8XoK1'), + ('thisisalongertestPW', '$P$912345678LIjjb6PhecupozNBmDndU0'), + ('JohnRipper', '$P$612345678si5M0DDyPpmRCmcltU/YW/'), + ('JohnRipper', '$H$712345678WhEyvy1YWzT4647jzeOmo0'), + ('JohnRipper', '$P$B12345678L6Lpt4BxNotVIMILOa9u81'), + + # + # custom + # + ('', '$P$7JaFQsPzJSuenezefD/3jHgt5hVfNH0'), + ('compL3X!', '$P$FiS0N5L672xzQx1rt1vgdJQRYKnQM9/'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$P$7SMy8VxnfsIy2Sxm7fJxDSdil.h7TW.'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + # ---\/ + '$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r!L0', + ] + +#============================================================================= +# plaintext +#============================================================================= +class plaintext_test(HandlerCase): + # TODO: integrate EncodingHandlerMixin + handler = hash.plaintext + accepts_all_hashes = True + + known_correct_hashes = [ + ('',''), + ('password', 'password'), + + # ensure unicode uses utf-8 + (UPASS_TABLE, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + (PASS_TABLE_UTF8, UPASS_TABLE if PY3 else PASS_TABLE_UTF8), + ] + +#============================================================================= +# postgres_md5 +#============================================================================= +class postgres_md5_test(UserHandlerMixin, HandlerCase): + handler = hash.postgres_md5 + known_correct_hashes = [ + # ((secret,user),hash) + + # + # generated using postgres 8.1 + # + (('mypass', 'postgres'), 'md55fba2ea04fd36069d2574ea71c8efe9d'), + (('mypass', 'root'), 'md540c31989b20437833f697e485811254b'), + (("testpassword",'testuser'), 'md5d4fc5129cc2c25465a5370113ae9835f'), + + # + # custom + # + + # verify unicode->utf8 + ((UPASS_TABLE, 'postgres'), 'md5cb9f11283265811ce076db86d18a22d2'), + ] + known_unidentified_hashes = [ + # bad 'z' char in otherwise correct hash + 'md54zc31989b20437833f697e485811254b', + ] + +#============================================================================= +# scram hash +#============================================================================= +class scram_test(HandlerCase): + handler = hash.scram + + # TODO: need a bunch more reference vectors from some real + # SCRAM transactions. + known_correct_hashes = [ + # + # taken from example in SCRAM specification (rfc 5802) + # + ('pencil', '$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'), + + # + # custom + # + + # same as 5802 example hash, but with sha-256 & sha-512 added. + ('pencil', '$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ'), + + # test unicode passwords & saslprep (all the passwords below + # should normalize to the same value: 'IX \xE0') + (u('IX \xE0'), '$scram$6400$0BojBCBE6P2/N4bQ$' + 'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'), + (u('\u2168\u3000a\u0300'), '$scram$6400$0BojBCBE6P2/N4bQ$' + 'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'), + (u('\u00ADIX \xE0'), '$scram$6400$0BojBCBE6P2/N4bQ$' + 'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'), + ] + + known_malformed_hashes = [ + # zero-padding in rounds + '$scram$04096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', + + # non-digit in rounds + '$scram$409A$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', + + # bad char in salt ---\/ + '$scram$4096$QSXCR.Q6sek8bf9-$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', + + # bad char in digest ---\/ + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX3-', + + # missing sections + '$scram$4096$QSXCR.Q6sek8bf92', + '$scram$4096$QSXCR.Q6sek8bf92$', + + # too many sections + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30$', + + # missing separator + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY', + + # too many chars in alg name + '$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'shaxxx-190=HZbuOlKbWl.eR8AfIposuKbhX30', + + # missing sha-1 alg + '$scram$4096$QSXCR.Q6sek8bf92$sha-256=HZbuOlKbWl.eR8AfIposuKbhX30', + + # non-iana name + '$scram$4096$QSXCR.Q6sek8bf92$sha1=HZbuOlKbWl.eR8AfIposuKbhX30', + ] + + def setUp(self): + super(scram_test, self).setUp() + + # some platforms lack stringprep (e.g. Jython, IronPython) + self.require_stringprep() + + # silence norm_hash_name() warning + warnings.filterwarnings("ignore", r"norm_hash_name\(\): unknown hash") + + def test_90_algs(self): + "test parsing of 'algs' setting" + defaults = dict(salt=b('A')*10, rounds=1000) + def parse(algs, **kwds): + for k in defaults: + kwds.setdefault(k, defaults[k]) + return self.handler(algs=algs, **kwds).algs + + # None -> default list + self.assertEqual(parse(None, use_defaults=True), hash.scram.default_algs) + self.assertRaises(TypeError, parse, None) + + # strings should be parsed + self.assertEqual(parse("sha1"), ["sha-1"]) + self.assertEqual(parse("sha1, sha256, md5"), ["md5","sha-1","sha-256"]) + + # lists should be normalized + self.assertEqual(parse(["sha-1","sha256"]), ["sha-1","sha-256"]) + + # sha-1 required + self.assertRaises(ValueError, parse, ["sha-256"]) + self.assertRaises(ValueError, parse, algs=[], use_defaults=True) + + # alg names must be < 10 chars + self.assertRaises(ValueError, parse, ["sha-1","shaxxx-190"]) + + # alg & checksum mutually exclusive. + self.assertRaises(RuntimeError, parse, ['sha-1'], + checksum={"sha-1": b("\x00"*20)}) + + def test_90_checksums(self): + "test internal parsing of 'checksum' keyword" + # check non-bytes checksum values are rejected + self.assertRaises(TypeError, self.handler, use_defaults=True, + checksum={'sha-1': u('X')*20}) + + # check sha-1 is required + self.assertRaises(ValueError, self.handler, use_defaults=True, + checksum={'sha-256': b('X')*32}) + + # XXX: anything else that's not tested by the other code already? + + def test_91_extract_digest_info(self): + "test scram.extract_digest_info()" + edi = self.handler.extract_digest_info + + # return appropriate value or throw KeyError + h = "$scram$10$AAAAAA$sha-1=AQ,bbb=Ag,ccc=Aw" + s = b('\x00')*4 + self.assertEqual(edi(h,"SHA1"), (s,10, b('\x01'))) + self.assertEqual(edi(h,"bbb"), (s,10, b('\x02'))) + self.assertEqual(edi(h,"ccc"), (s,10, b('\x03'))) + self.assertRaises(KeyError, edi, h, "ddd") + + # config strings should cause value error. + c = "$scram$10$....$sha-1,bbb,ccc" + self.assertRaises(ValueError, edi, c, "sha-1") + self.assertRaises(ValueError, edi, c, "bbb") + self.assertRaises(ValueError, edi, c, "ddd") + + def test_92_extract_digest_algs(self): + "test scram.extract_digest_algs()" + eda = self.handler.extract_digest_algs + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'), ["sha-1"]) + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', format="hashlib"), + ["sha1"]) + + self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ'), + ["sha-1","sha-256","sha-512"]) + + def test_93_derive_digest(self): + "test scram.derive_digest()" + # NOTE: this just does a light test, since derive_digest + # is used by encrypt / verify, and is tested pretty well via those. + + hash = self.handler.derive_digest + + # check various encodings of password work. + s1 = b('\x01\x02\x03') + d1 = b('\xb2\xfb\xab\x82[tNuPnI\x8aZZ\x19\x87\xcen\xe9\xd3') + self.assertEqual(hash(u("\u2168"), s1, 1000, 'sha-1'), d1) + self.assertEqual(hash(b("\xe2\x85\xa8"), s1, 1000, 'SHA-1'), d1) + self.assertEqual(hash(u("IX"), s1, 1000, 'sha1'), d1) + self.assertEqual(hash(b("IX"), s1, 1000, 'SHA1'), d1) + + # check algs + self.assertEqual(hash("IX", s1, 1000, 'md5'), + b('3\x19\x18\xc0\x1c/\xa8\xbf\xe4\xa3\xc2\x8eM\xe8od')) + self.assertRaises(ValueError, hash, "IX", s1, 1000, 'sha-666') + + # check rounds + self.assertRaises(ValueError, hash, "IX", s1, 0, 'sha-1') + + # bad types + self.assertRaises(TypeError, hash, "IX", u('\x01'), 1000, 'md5') + + def test_94_saslprep(self): + "test encrypt/verify use saslprep" + # NOTE: this just does a light test that saslprep() is being + # called in various places, relying in saslpreps()'s tests + # to verify full normalization behavior. + + # encrypt unnormalized + h = self.do_encrypt(u("I\u00ADX")) + self.assertTrue(self.do_verify(u("IX"), h)) + self.assertTrue(self.do_verify(u("\u2168"), h)) + + # encrypt normalized + h = self.do_encrypt(u("\xF3")) + self.assertTrue(self.do_verify(u("o\u0301"), h)) + self.assertTrue(self.do_verify(u("\u200Do\u0301"), h)) + + # throws error if forbidden char provided + self.assertRaises(ValueError, self.do_encrypt, u("\uFDD0")) + self.assertRaises(ValueError, self.do_verify, u("\uFDD0"), h) + + def test_95_context_algs(self): + "test handling of 'algs' in context object" + handler = self.handler + from passlib.context import CryptContext + c1 = CryptContext(["scram"], scram__algs="sha1,md5") + + h = c1.encrypt("dummy") + self.assertEqual(handler.extract_digest_algs(h), ["md5", "sha-1"]) + self.assertFalse(c1.needs_update(h)) + + c2 = c1.copy(scram__algs="sha1") + self.assertFalse(c2.needs_update(h)) + + c2 = c1.copy(scram__algs="sha1,sha256") + self.assertTrue(c2.needs_update(h)) + + def test_96_full_verify(self): + "test verify(full=True) flag" + def vpart(s, h): + return self.handler.verify(s, h) + def vfull(s, h): + return self.handler.verify(s, h, full=True) + + # reference + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertTrue(vfull('pencil', h)) + self.assertFalse(vfull('tape', h)) + + # catch truncated digests. + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhV,' # -1 char + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertRaises(ValueError, vfull, 'pencil', h) + + # catch padded digests. + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' + 'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVYa,' # +1 char + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertRaises(ValueError, vfull, 'pencil', h) + + # catch hash containing digests belonging to diff passwords. + # proper behavior for quick-verify (the default) is undefined, + # but full-verify should throw error. + h = ('$scram$4096$QSXCR.Q6sek8bf92$' + 'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' # 'pencil' + 'sha-256=R7RJDWIbeKRTFwhE9oxh04kab0CllrQ3kCcpZUcligc,' # 'tape' + 'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' # 'pencil' + 'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ') + self.assertTrue(vpart('tape', h)) + self.assertFalse(vpart('pencil', h)) + self.assertRaises(ValueError, vfull, 'pencil', h) + self.assertRaises(ValueError, vfull, 'tape', h) + +#============================================================================= +# (netbsd's) sha1 crypt +#============================================================================= +class _sha1_crypt_test(HandlerCase): + handler = hash.sha1_crypt + + known_correct_hashes = [ + # + # custom + # + ("password", "$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"), + ("password", "$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH"), + (UPASS_TABLE, '$sha1$40000$uJ3Sp7LE$.VEmLO5xntyRFYihC7ggd3297T/D'), + ] + + known_malformed_hashes = [ + # bad char in otherwise correct hash + '$sha1$21773$u!7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', + + # zero padded rounds + '$sha1$01773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', + + # too many fields + '$sha1$21773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$', + + # empty rounds field + '$sha1$$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH$', + ] + + platform_crypt_support = [ + ("netbsd", True), + ("freebsd|openbsd|linux|solaris|darwin", False), + ] + +sha1_crypt_os_crypt_test, sha1_crypt_builtin_test = \ + _sha1_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +#============================================================================= +# roundup +#============================================================================= + +# NOTE: all roundup hashes use PrefixWrapper, +# so there's nothing natively to test. +# so we just have a few quick cases... +from passlib.handlers import roundup + +class RoundupTest(TestCase): + + def _test_pair(self, h, secret, hash): + self.assertTrue(h.verify(secret, hash)) + self.assertFalse(h.verify('x'+secret, hash)) + + def test_pairs(self): + self._test_pair( + hash.ldap_hex_sha1, + "sekrit", + '{SHA}8d42e738c7adee551324955458b5e2c0b49ee655') + + self._test_pair( + hash.ldap_hex_md5, + "sekrit", + '{MD5}ccbc53f4464604e714f69dd11138d8b5') + + self._test_pair( + hash.ldap_des_crypt, + "sekrit", + '{CRYPT}nFia0rj2TT59A') + + self._test_pair( + hash.roundup_plaintext, + "sekrit", + '{plaintext}sekrit') + + self._test_pair( + hash.ldap_pbkdf2_sha1, + "sekrit", + '{PBKDF2}5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE') + +#============================================================================= +# sha256-crypt +#============================================================================= +class _sha256_crypt_test(HandlerCase): + handler = hash.sha256_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9'), + ('U*U***U', '$5$LKO/Ute40T3FNF95$fdgfoJEBoMajNxCv3Ru9LyQ0xZgv0OBMQoq80LQ/Qd.'), + ('U*U***U*', '$5$LKO/Ute40T3FNF95$8Ry82xGnnPI/6HtFYnvPBTYgOL23sdMXn8C29aO.x/A'), + ('*U*U*U*U', '$5$9mx1HkCz7G1xho50$O7V7YgleJKLUhcfk9pgzdh3RapEaWqMtEp9UUBAKIPA'), + ('', '$5$kc7lRD1fpYg0g.IP$d7CMTcEqJyTXyeq8hTdu/jB/I6DGkoo62NXbHIR7S43'), + + # + # custom tests + # + ('', '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'), + (' ', '$5$rounds=10376$I5lNtXtRmf.OoMd8$Ko3AI1VvTANdyKhBPavaRjJzNpSatKU6QVN9uwS9MH.'), + ('test', '$5$rounds=11858$WH1ABM5sKhxbkgCK$aTQsjPkz0rBsH3lQlJxw9HDTDXPKBxC0LlVeV69P.t1'), + ('Compl3X AlphaNu3meric', '$5$rounds=10350$o.pwkySLCzwTdmQX$nCMVsnF3TXWcBPOympBUUSQi6LGGloZoOsVJMGJ09UB'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$5$rounds=11944$9dhlu07dQMRWvTId$LyUI5VWkGFwASlzntk1RLurxX54LUhgAcJZIt0pYGT7'), + (u('with unic\u00D6de'), '$5$rounds=1000$IbG0EuGQXw5EkMdP$LQ5AfPf13KufFsKtmazqnzSGZ4pxtUNw3woQ.ELRDF4'), + ] + + if TEST_MODE("full"): + # builtin alg was changed in 1.6, and had possibility of fencepost + # errors near rounds that are multiples of 42. these hashes test rounds + # 1004..1012 (42*24=1008 +/- 4) to ensure no mistakes were made. + # (also relying on fuzz testing against os_crypt backend). + known_correct_hashes.extend([ + ("secret", '$5$rounds=1004$nacl$oiWPbm.kQ7.jTCZoOtdv7/tO5mWv/vxw5yTqlBagVR7'), + ("secret", '$5$rounds=1005$nacl$6Mo/TmGDrXxg.bMK9isRzyWH3a..6HnSVVsJMEX7ud/'), + ("secret", '$5$rounds=1006$nacl$I46VwuAiUBwmVkfPFakCtjVxYYaOJscsuIeuZLbfKID'), + ("secret", '$5$rounds=1007$nacl$9fY4j1AV3N/dV/YMUn1enRHKH.7nEL4xf1wWB6wfDD4'), + ("secret", '$5$rounds=1008$nacl$CiFWCfn8ODmWs0I1xAdXFo09tM8jr075CyP64bu3by9'), + ("secret", '$5$rounds=1009$nacl$QtpFX.CJHgVQ9oAjVYStxAeiU38OmFILWm684c6FyED'), + ("secret", '$5$rounds=1010$nacl$ktAwXuT5WbjBW/0ZU1eNMpqIWY1Sm4twfRE1zbZyo.B'), + ("secret", '$5$rounds=1011$nacl$QJWLBEhO9qQHyMx4IJojSN9sS41P1Yuz9REddxdO721'), + ("secret", '$5$rounds=1012$nacl$mmf/k2PkbBF4VCtERgky3bEVavmLZKFwAcvxD1p3kV2'), + ]) + + known_malformed_hashes = [ + # bad char in otherwise correct hash + '$5$rounds=10428$uy/:jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMeZGsGx2aBvxTvDFI613c3', + + # zero-padded rounds + '$5$rounds=010428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3', + + # extra "$" + '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3$', + ] + + known_correct_configs = [ + # config, secret, result + + # + # taken from official specification at http://www.akkadia.org/drepper/SHA-crypt.txt + # + ( "$5$saltstring", "Hello world!", + "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5" ), + ( "$5$rounds=10000$saltstringsaltstring", "Hello world!", + "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2." + "opqey6IcA" ), + ( "$5$rounds=5000$toolongsaltstring", "This is just a test", + "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8" + "mGRcvxa5" ), + ( "$5$rounds=1400$anotherlongsaltstring", + "a very much longer text to encrypt. This one even stretches over more" + "than one line.", + "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12" + "oP84Bnq1" ), + ( "$5$rounds=77777$short", + "we have a short salt string but not a short password", + "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/" ), + ( "$5$rounds=123456$asaltof16chars..", "a short string", + "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/" + "cZKmF/wJvD" ), + ( "$5$rounds=10$roundstoolow", "the minimum number is still observed", + "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL97" + "2bIC" ), + ] + + filter_config_warnings = True # rounds too low, salt too small + + platform_crypt_support = [ + ("freebsd(9|1\d)|linux", True), + ("freebsd8", None), # added in freebsd 8.3 + ("freebsd|openbsd|netbsd|darwin", False), + # solaris - depends on policy + ] + +sha256_crypt_os_crypt_test, sha256_crypt_builtin_test = \ + _sha256_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +#============================================================================= +# test sha512-crypt +#============================================================================= +class _sha512_crypt_test(HandlerCase): + handler = hash.sha512_crypt + + known_correct_hashes = [ + # + # from JTR 1.7.9 + # + ('U*U*U*U*', "$6$LKO/Ute40T3FNF95$6S/6T2YuOIHY0N3XpLKABJ3soYcXD9mB7uVbtEZDj/LNscVhZoZ9DEH.sBciDrMsHOWOoASbNLTypH/5X26gN0"), + ('U*U***U', "$6$LKO/Ute40T3FNF95$wK80cNqkiAUzFuVGxW6eFe8J.fSVI65MD5yEm8EjYMaJuDrhwe5XXpHDJpwF/kY.afsUs1LlgQAaOapVNbggZ1"), + ('U*U***U*', "$6$LKO/Ute40T3FNF95$YS81pp1uhOHTgKLhSMtQCr2cDiUiN03Ud3gyD4ameviK1Zqz.w3oXsMgO6LrqmIEcG3hiqaUqHi/WEE2zrZqa/"), + ('*U*U*U*U', "$6$OmBOuxFYBZCYAadG$WCckkSZok9xhp4U1shIZEV7CCVwQUwMVea7L3A77th6SaE9jOPupEMJB.z0vIWCDiN9WLh2m9Oszrj5G.gt330"), + ('', "$6$ojWH1AiTee9x1peC$QVEnTvRVlPRhcLQCk/HnHaZmlGAAjCfrAN0FtOsOnUk5K5Bn/9eLHHiRzrTzaIKjW9NTLNIBUCtNVOowWS2mN."), + + # + # custom tests + # + ('', '$6$rounds=11021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1'), + (' ', '$6$rounds=11104$ED9SA4qGmd57Fq2m$q/.PqACDM/JpAHKmr86nkPzzuR5.YpYa8ZJJvI8Zd89ZPUYTJExsFEIuTYbM7gAGcQtTkCEhBKmp1S1QZwaXx0'), + ('test', '$6$rounds=11531$G/gkPn17kHYo0gTF$Kq.uZBHlSBXyzsOJXtxJruOOH4yc0Is13uY7yK0PvAvXxbvc1w8DO1RzREMhKsc82K/Jh8OquV8FZUlreYPJk1'), + ('Compl3X AlphaNu3meric', '$6$rounds=10787$wakX8nGKEzgJ4Scy$X78uqaX1wYXcSCtS4BVYw2trWkvpa8p7lkAtS9O/6045fK4UB2/Jia0Uy/KzCpODlfVxVNZzCCoV9s2hoLfDs/'), + ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', '$6$rounds=11065$5KXQoE1bztkY5IZr$Jf6krQSUKKOlKca4hSW07MSerFFzVIZt/N3rOTsUgKqp7cUdHrwV8MoIVNCk9q9WL3ZRMsdbwNXpVk0gVxKtz1'), + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$6$rounds=40000$PEZTJDiyzV28M3.m$GTlnzfzGB44DGd1XqlmC4erAJKCP.rhvLvrYxiT38htrNzVGBnplFOHjejUGVrCfusGWxLQCc3pFO0A/1jYYr0'), + ] + + known_malformed_hashes = [ + # zero-padded rounds + '$6$rounds=011021$KsvQipYPWpr93wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', + # bad char in otherwise correct hash + '$6$rounds=11021$KsvQipYPWpr9:wWP$v7xjI4X6vyVptJjB1Y02vZC5SaSijBkGmq1uJhPr3cvqvvkd42Xvo48yLVPFt8dvhCsnlUgpX.//Cxn91H4qy1', + ] + + known_correct_configs = [ + # config, secret, result + + # + # taken from official specification at http://www.akkadia.org/drepper/SHA-crypt.txt + # + ("$6$saltstring", "Hello world!", + "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu" + "esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" ), + + ( "$6$rounds=10000$saltstringsaltstring", "Hello world!", + "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb" + "HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." ), + + ( "$6$rounds=5000$toolongsaltstring", "This is just a test", + "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQ" + "zQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0" ), + + ( "$6$rounds=1400$anotherlongsaltstring", + "a very much longer text to encrypt. This one even stretches over more" + "than one line.", + "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wP" + "vMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1" ), + + ( "$6$rounds=77777$short", + "we have a short salt string but not a short password", + "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g" + "ge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0" ), + + ( "$6$rounds=123456$asaltof16chars..", "a short string", + "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc" + "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1" ), + + ( "$6$rounds=10$roundstoolow", "the minimum number is still observed", + "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x" + "hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." ), + ] + + filter_config_warnings = True # rounds too low, salt too small + + platform_crypt_support = _sha256_crypt_test.platform_crypt_support + +sha512_crypt_os_crypt_test, sha512_crypt_builtin_test = \ + _sha512_crypt_test.create_backend_cases(["os_crypt","builtin"]) + +#============================================================================= +# sun md5 crypt +#============================================================================= +class sun_md5_crypt_test(HandlerCase): + handler = hash.sun_md5_crypt + + # TODO: this scheme needs some real test vectors, especially due to + # the "bare salt" issue which plagued the official parser. + known_correct_hashes = [ + # + # http://forums.halcyoninc.com/showthread.php?t=258 + # + ("Gpcs3_adm", "$md5$zrdhpMlZ$$wBvMOEqbSjU.hu5T2VEP01"), + + # + # http://www.c0t0d0s0.org/archives/4453-Less-known-Solaris-features-On-passwords-Part-2-Using-stronger-password-hashing.html + # + ("aa12345678", "$md5$vyy8.OVF$$FY4TWzuauRl4.VQNobqMY."), + + # + # http://www.cuddletech.com/blog/pivot/entry.php?id=778 + # + ("this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), + + # + # http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 + # + ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), + + # + # source: http://solaris-training.com/301_HTML/docs/deepdiv.pdf page 27 + # FIXME: password unknown + # "$md5,rounds=8000$kS9FT1JC$$mnUrRO618lLah5iazwJ9m1" + + # + # source: http://www.visualexams.com/310-303.htm + # XXX: this has 9 salt chars unlike all other hashes. is that valid? + # FIXME: password unknown + # "$md5,rounds=2006$2amXesSj5$$kCF48vfPsHDjlKNXeEw7V." + # + + # + # custom + # + + # ensures utf-8 used for unicode + (UPASS_TABLE, '$md5,rounds=5000$10VYDzAA$$1arAVtMA3trgE1qJ2V0Ez1'), + ] + + known_correct_configs = [ + # (config, secret, hash) + + #--------------------------- + # test salt string handling + # + # these tests attempt to verify that passlib is handling + # the "bare salt" issue (see sun md5 crypt docs) + # in a sane manner + #--------------------------- + + # config with "$" suffix, hash strings with "$$" suffix, + # should all be treated the same, with one "$" added to salt digest. + ("$md5$3UqYqndY$", + "this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), + ("$md5$3UqYqndY$$......................", + "this", "$md5$3UqYqndY$$6P.aaWOoucxxq.l00SS9k0"), + + # config with no suffix, hash strings with "$" suffix, + # should all be treated the same, and no suffix added to salt digest. + # NOTE: this is just a guess re: config w/ no suffix, + # but otherwise there's no sane way to encode bare_salt=False + # within config string. + ("$md5$3UqYqndY", + "this", "$md5$3UqYqndY$HIZVnfJNGCPbDZ9nIRSgP1"), + ("$md5$3UqYqndY$......................", + "this", "$md5$3UqYqndY$HIZVnfJNGCPbDZ9nIRSgP1"), + ] + + known_malformed_hashes = [ + # unexpected end of hash + "$md5,rounds=5000", + + # bad rounds + "$md5,rounds=500A$xxxx", + "$md5,rounds=0500$xxxx", + "$md5,rounds=0$xxxx", + + # bad char in otherwise correct hash + "$md5$RPgL!6IJ$WTvAlUJ7MqH5xak2FMEwS/", + + # digest too short + "$md5$RPgLa6IJ$WTvAlUJ7MqH5xak2FMEwS", + + # digest too long + "$md5$RPgLa6IJ$WTvAlUJ7MqH5xak2FMEwS/.", + + # 2+ "$" at end of salt in config + # NOTE: not sure what correct behavior is, so forbidding format for now. + "$md5$3UqYqndY$$", + + # 3+ "$" at end of salt in hash + # NOTE: not sure what correct behavior is, so forbidding format for now. + "$md5$RPgLa6IJ$$$WTvAlUJ7MqH5xak2FMEwS/", + + ] + + platform_crypt_support = [ + ("solaris", True), + ("freebsd|openbsd|netbsd|linux|darwin", False), + ] + + def do_verify(self, secret, hash): + # override to fake error for "$..." hash strings listed in known_config. + # these have to be hash strings, in order to test bare salt issue. + if isinstance(hash, str) and hash.endswith("$......................"): + raise ValueError("pretending '$.' hash is config string") + return self.handler.verify(secret, hash) + +#============================================================================= +# unix disabled / fallback +#============================================================================= +class unix_disabled_test(HandlerCase): + handler = hash.unix_disabled +# accepts_all_hashes = True # TODO: turn this off. + is_disabled_handler = True + + known_correct_hashes = [ + # everything should hash to "!" (or "*" on BSD), + # and nothing should verify against either string + ("password", "!"), + (UPASS_TABLE, "*"), + ] + + known_unidentified_hashes = [ + # should never identify anything crypt() could return... + "$1$xxx", + "abc", + "./az", + "{SHA}xxx", + ] + + def test_76_hash_border(self): + # so empty strings pass + self.accepts_all_hashes = True + super(unix_disabled_test, self).test_76_hash_border() + + def test_90_special(self): + "test marker option & special behavior" + handler = self.handler + + # preserve hash if provided + self.assertEqual(handler.genhash("stub", "!asd"), "!asd") + + # use marker if no hash + self.assertEqual(handler.genhash("stub", None), handler.default_marker) + + # custom marker + self.assertEqual(handler.genhash("stub", None, marker="*xxx"), "*xxx") + + # reject invalid marker + self.assertRaises(ValueError, handler.genhash, 'stub', None, marker='abc') + +class unix_fallback_test(HandlerCase): + handler = hash.unix_fallback + accepts_all_hashes = True + is_disabled_handler = True + + known_correct_hashes = [ + # *everything* should hash to "!", and nothing should verify + ("password", "!"), + (UPASS_TABLE, "!"), + ] + + # silence annoying deprecation warning + def setUp(self): + super(unix_fallback_test, self).setUp() + warnings.filterwarnings("ignore", "'unix_fallback' is deprecated") + + def test_90_wildcard(self): + "test enable_wildcard flag" + h = self.handler + self.assertTrue(h.verify('password','', enable_wildcard=True)) + self.assertFalse(h.verify('password','')) + for c in ("!*x"): + self.assertFalse(h.verify('password',c, enable_wildcard=True)) + self.assertFalse(h.verify('password',c)) + + def test_91_preserves_existing(self): + "test preserves existing disabled hash" + handler = self.handler + + # use marker if no hash + self.assertEqual(handler.genhash("stub", None), "!") + + # use hash if provided and valid + self.assertEqual(handler.genhash("stub", "!asd"), "!asd") + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_hosts.py passlib-1.6.1/passlib/tests/test_hosts.py --- passlib-1.5.3/passlib/tests/test_hosts.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_hosts.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,37 +1,38 @@ """test passlib.hosts""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import logging; log = logging.getLogger(__name__) -#site -#pkg +import warnings +# site +# pkg from passlib import hosts, hash as hashmod from passlib.utils import unix_crypt_schemes from passlib.tests.utils import TestCase -#module +# module -#========================================================= -#test predefined app contexts -#========================================================= +#============================================================================= +# test predefined app contexts +#============================================================================= class HostsTest(TestCase): "perform general tests to make sure contexts work" - #NOTE: these tests are not really comprehensive, - # since they would do little but duplicate - # the presets in apps.py + # NOTE: these tests are not really comprehensive, + # since they would do little but duplicate + # the presets in apps.py # - # they mainly try to ensure no typos - # or dynamic behavior foul-ups. + # they mainly try to ensure no typos + # or dynamic behavior foul-ups. - def check_unix_fallback(self, ctx): + def check_unix_disabled(self, ctx): for hash in [ "", "!", "*", "!$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0", ]: - self.assertEqual(ctx.identify(hash), 'unix_fallback') + self.assertEqual(ctx.identify(hash), 'unix_disabled') self.assertFalse(ctx.verify('test', hash)) def test_linux_context(self): @@ -45,7 +46,7 @@ 'kAJJz.Rwp0A/I', ]: self.assertTrue(ctx.verify("test", hash)) - self.check_unix_fallback(ctx) + self.check_unix_disabled(ctx) def test_bsd_contexts(self): for ctx in [ @@ -58,12 +59,12 @@ 'kAJJz.Rwp0A/I', ]: self.assertTrue(ctx.verify("test", hash)) - h1 = '$2a$10$Ljj0Kgu7Ddob9xWoqzn0ae.uNfxPRofowWdksk.6jCUHKTGYLD.QG' + h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" if hashmod.bcrypt.has_backend(): self.assertTrue(ctx.verify("test", h1)) else: self.assertEqual(ctx.identify(h1), "bcrypt") - self.check_unix_fallback(ctx) + self.check_unix_disabled(ctx) def test_host_context(self): ctx = getattr(hosts, "host_context", None) @@ -71,16 +72,16 @@ return self.skipTest("host_context not available on this platform") # validate schemes is non-empty, - # and contains unix_fallback + at least one real scheme - schemes = ctx.policy.schemes() + # and contains unix_disabled + at least one real scheme + schemes = list(ctx.schemes()) self.assertTrue(schemes, "appears to be unix system, but no known schemes supported by crypt") - self.assertTrue('unix_fallback' in schemes) - schemes.remove("unix_fallback") + self.assertTrue('unix_disabled' in schemes) + schemes.remove("unix_disabled") self.assertTrue(schemes, "should have schemes beside fallback scheme") self.assertTrue(set(unix_crypt_schemes).issuperset(schemes)) # check for hash support - self.check_unix_fallback(ctx) + self.check_unix_disabled(ctx) for scheme, hash in [ ("sha512_crypt", ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751')), @@ -92,6 +93,6 @@ if scheme in schemes: self.assertTrue(ctx.verify("test", hash)) -#========================================================= -#eof -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_registry.py passlib-1.6.1/passlib/tests/test_registry.py --- passlib-1.5.3/passlib/tests/test_registry.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_registry.py 2012-08-02 18:01:16.000000000 +0000 @@ -1,32 +1,32 @@ """tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import hashlib from logging import getLogger import os import time import warnings import sys -#site -#pkg +# site +# pkg from passlib import hash, registry from passlib.registry import register_crypt_handler, register_crypt_handler_path, \ get_crypt_handler, list_crypt_handlers, _unload_handler_name as unload_handler_name import passlib.utils.handlers as uh -from passlib.tests.utils import TestCase, mktemp, catch_warnings -#module +from passlib.tests.utils import TestCase, catch_warnings +# module log = getLogger(__name__) -#========================================================= -#dummy handlers +#============================================================================= +# dummy handlers # -#NOTE: these are defined outside of test case -# since they're used by test_register_crypt_handler_path(), -# which needs them to be available as module globals. -#========================================================= +# NOTE: these are defined outside of test case +# since they're used by test_register_crypt_handler_path(), +# which needs them to be available as module globals. +#============================================================================= class dummy_0(uh.StaticHandler): name = "dummy_0" @@ -35,12 +35,12 @@ dummy_x = 1 -#========================================================= -#test registry -#========================================================= +#============================================================================= +# test registry +#============================================================================= class RegistryTest(TestCase): - case_prefix = "passlib registry" + descriptionPrefix = "passlib registry" def tearDown(self): for name in ("dummy_0", "dummy_1", "dummy_x", "dummy_bad"): @@ -48,17 +48,17 @@ def test_hash_proxy(self): "test passlib.hash proxy object" - #check dir works + # check dir works dir(hash) - #check repr works + # check repr works repr(hash) - #check non-existent attrs raise error + # check non-existent attrs raise error self.assertRaises(AttributeError, getattr, hash, 'fooey') - #GAE tries to set __loader__, - #make sure that doesn't call register_crypt_handler. + # GAE tries to set __loader__, + # make sure that doesn't call register_crypt_handler. old = getattr(hash, "__loader__", None) test = object() hash.__loader__ = test @@ -70,25 +70,33 @@ hash.__loader__ = old self.assertIs(hash.__loader__, old) - #check storing attr calls register_crypt_handler + # check storing attr calls register_crypt_handler class dummy_1(uh.StaticHandler): name = "dummy_1" hash.dummy_1 = dummy_1 self.assertIs(get_crypt_handler("dummy_1"), dummy_1) - #check storing under wrong name results in error + # check storing under wrong name results in error self.assertRaises(ValueError, setattr, hash, "dummy_1x", dummy_1) def test_register_crypt_handler_path(self): "test register_crypt_handler_path()" - #NOTE: this messes w/ internals of registry, shouldn't be used publically. - paths = registry._handler_locations + # NOTE: this messes w/ internals of registry, shouldn't be used publically. + paths = registry._locations - #check namespace is clear + # check namespace is clear self.assertTrue('dummy_0' not in paths) self.assertFalse(hasattr(hash, 'dummy_0')) - #try lazy load + # check invalid names are rejected + self.assertRaises(ValueError, register_crypt_handler_path, + "dummy_0", ".test_registry") + self.assertRaises(ValueError, register_crypt_handler_path, + "dummy_0", __name__ + ":dummy_0:xxx") + self.assertRaises(ValueError, register_crypt_handler_path, + "dummy_0", __name__ + ":dummy_0.xxx") + + # try lazy load register_crypt_handler_path('dummy_0', __name__) self.assertTrue('dummy_0' in list_crypt_handlers()) self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True)) @@ -96,20 +104,20 @@ self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True)) unload_handler_name('dummy_0') - #try lazy load w/ alt + # try lazy load w/ alt register_crypt_handler_path('dummy_0', __name__ + ':alt_dummy_0') self.assertIs(hash.dummy_0, alt_dummy_0) unload_handler_name('dummy_0') - #check lazy load w/ wrong type fails + # check lazy load w/ wrong type fails register_crypt_handler_path('dummy_x', __name__) self.assertRaises(TypeError, get_crypt_handler, 'dummy_x') - #check lazy load w/ wrong name fails + # check lazy load w/ wrong name fails register_crypt_handler_path('alt_dummy_0', __name__) self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0") - #TODO: check lazy load which calls register_crypt_handler (warning should be issued) + # TODO: check lazy load which calls register_crypt_handler (warning should be issued) sys.modules.pop("passlib.tests._test_bad_register", None) register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register") with catch_warnings(): @@ -123,9 +131,11 @@ self.assertRaises(TypeError, register_crypt_handler, {}) - self.assertRaises(ValueError, register_crypt_handler, uh.StaticHandler) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name=None))) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="AB_CD"))) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab-cd"))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab__cd"))) + self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="default"))) class dummy_1(uh.StaticHandler): name = "dummy_1" @@ -153,15 +163,41 @@ class dummy_1(uh.StaticHandler): name = "dummy_1" + # without available handler self.assertRaises(KeyError, get_crypt_handler, "dummy_1") + self.assertIs(get_crypt_handler("dummy_1", None), None) + # already loaded handler register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) with catch_warnings(): warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning) + + # already loaded handler, using incorrect name self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1) -#========================================================= -#EOF -#========================================================= + # lazy load of unloaded handler, using incorrect name + register_crypt_handler_path('dummy_0', __name__) + self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0) + + # check system & private names aren't returned + import passlib.hash # ensure module imported, so py3.3 sets __package__ + passlib.hash.__dict__["_fake"] = "dummy" # so behavior seen under py2x also + for name in ["_fake", "__package__"]: + self.assertRaises(KeyError, get_crypt_handler, name) + self.assertIs(get_crypt_handler(name, None), None) + + def test_list_crypt_handlers(self): + "test list_crypt_handlers()" + from passlib.registry import list_crypt_handlers + + # check system & private names aren't returned + import passlib.hash # ensure module imported, so py3.3 sets __package__ + passlib.hash.__dict__["_fake"] = "dummy" # so behavior seen under py2x also + for name in list_crypt_handlers(): + self.assertFalse(name.startswith("_"), "%r: " % name) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_utils.py passlib-1.6.1/passlib/tests/test_utils.py --- passlib-1.5.3/passlib/tests/test_utils.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_utils.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,49 +1,100 @@ """tests for passlib.util""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core from binascii import hexlify, unhexlify import sys import random import warnings -#site -#pkg -#module -from passlib.context import CryptContext -from passlib import utils -from passlib.utils import h64, des, Undef, sys_bits, bytes, b, \ - native_str, to_bytes, to_unicode, to_native_str, to_hash_str, \ - is_same_codec, is_ascii_safe, safe_os_crypt, md4 as md4_mod -from passlib.tests.utils import TestCase, Params as ak, \ - enable_option, catch_warnings +# site +# pkg +# module +from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \ + unicode, join_bytes, SUPPORTS_DIR_METHOD +from passlib.tests.utils import TestCase, catch_warnings def hb(source): return unhexlify(b(source)) -#========================================================= -#byte funcs -#========================================================= +#============================================================================= +# byte funcs +#============================================================================= class MiscTest(TestCase): "tests various parts of utils module" - #NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test + # NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test - def test_undef(self): - "test Undef singleton" - self.assertEqual(repr(Undef), "") - self.assertFalse(Undef==None,) - self.assertFalse(Undef==Undef,) - self.assertFalse(Undef==True,) - self.assertTrue(Undef!=None,) - self.assertTrue(Undef!=Undef,) - self.assertTrue(Undef!=True,) + def test_compat(self): + "test compat's lazymodule" + from passlib.utils import compat + # "" + self.assertRegex(repr(compat), + r"^$") + + # test synthentic dir() + dir(compat) + if SUPPORTS_DIR_METHOD: + self.assertTrue('UnicodeIO' in dir(compat)) + self.assertTrue('irange' in dir(compat)) + + def test_classproperty(self): + from passlib.utils import classproperty + + class test(object): + xvar = 1 + @classproperty + def xprop(cls): + return cls.xvar + + self.assertEqual(test.xprop, 1) + prop = test.__dict__['xprop'] + self.assertIs(prop.im_func, prop.__func__) + + def test_deprecated_function(self): + from passlib.utils import deprecated_function + # NOTE: not comprehensive, just tests the basic behavior + + @deprecated_function(deprecated="1.6", removed="1.8") + def test_func(*args): + "test docstring" + return args + + self.assertTrue(".. deprecated::" in test_func.__doc__) + + with self.assertWarningList(dict(category=DeprecationWarning, + message="the function passlib.tests.test_utils.test_func() " + "is deprecated as of Passlib 1.6, and will be " + "removed in Passlib 1.8." + )): + self.assertEqual(test_func(1,2), (1,2)) + + def test_memoized_property(self): + from passlib.utils import memoized_property + + class dummy(object): + counter = 0 + + @memoized_property + def value(self): + value = self.counter + self.counter = value+1 + return value + + d = dummy() + self.assertEqual(d.value, 0) + self.assertEqual(d.value, 0) + self.assertEqual(d.counter, 1) + + prop = dummy.value + self.assertIs(prop.im_func, prop.__func__) def test_getrandbytes(self): "test getrandbytes()" + from passlib.utils import getrandbytes, rng def f(*a,**k): - return utils.getrandbytes(utils.rng, *a, **k) + return getrandbytes(rng, *a, **k) self.assertEqual(len(f(0)), 0) a = f(10) b = f(10) @@ -54,207 +105,408 @@ def test_getrandstr(self): "test getrandstr()" + from passlib.utils import getrandstr, rng def f(*a,**k): - return utils.getrandstr(utils.rng, *a, **k) + return getrandstr(rng, *a, **k) - #count 0 + # count 0 self.assertEqual(f('abc',0), '') - #count <0 + # count <0 self.assertRaises(ValueError, f, 'abc', -1) - #letters 0 + # letters 0 self.assertRaises(ValueError, f, '', 0) - #letters 1 + # letters 1 self.assertEqual(f('a',5), 'aaaaa') - #letters - x = f(u'abc', 16) - y = f(u'abc', 16) + # letters + x = f(u('abc'), 16) + y = f(u('abc'), 16) self.assertIsInstance(x, unicode) self.assertNotEqual(x,y) - self.assertEqual(sorted(set(x)), [u'a',u'b',u'c']) + self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')]) - #bytes + # bytes x = f(b('abc'), 16) y = f(b('abc'), 16) self.assertIsInstance(x, bytes) self.assertNotEqual(x,y) - #NOTE: decoding this due to py3 bytes - self.assertEqual(sorted(set(x.decode("ascii"))), [u'a',u'b',u'c']) + # NOTE: decoding this due to py3 bytes + self.assertEqual(sorted(set(x.decode("ascii"))), [u('a'),u('b'),u('c')]) - #generate_password - self.assertEqual(len(utils.generate_password(15)), 15) + # generate_password + from passlib.utils import generate_password + self.assertEqual(len(generate_password(15)), 15) def test_is_crypt_context(self): "test is_crypt_context()" + from passlib.utils import is_crypt_context + from passlib.context import CryptContext cc = CryptContext(["des_crypt"]) - self.assertTrue(utils.is_crypt_context(cc)) - self.assertFalse(not utils.is_crypt_context(cc)) + self.assertTrue(is_crypt_context(cc)) + self.assertFalse(not is_crypt_context(cc)) def test_genseed(self): "test genseed()" - rng = utils.random.Random(utils.genseed()) + import random + from passlib.utils import genseed + rng = random.Random(genseed()) a = rng.randint(0, 100000) - rng = utils.random.Random(utils.genseed()) + rng = random.Random(genseed()) b = rng.randint(0, 100000) self.assertNotEqual(a,b) - rng.seed(utils.genseed(rng)) + rng.seed(genseed(rng)) - def test_safe_os_crypt(self): - "test safe_os_crypt() wrapper" - if not safe_os_crypt: - raise self.skipTest("stdlib crypt module not available") - - #NOTE: this is assuming EVERY crypt will support des_crypt. - # if this fails on some platform, this test will need modifying. - - #test normal case - ok, hash = safe_os_crypt(u'test', u'aa') - self.assertTrue(ok) - self.assertIsInstance(hash, unicode) - self.assertEqual(hash, u'aaqPiZY5xR5l.') - - #test hash-as-bytes - self.assertRaises(TypeError, safe_os_crypt, u'test', b('aa')) - - #test password as ascii - ret = safe_os_crypt(b('test'), u'aa') - self.assertEqual(ret, (True, u'aaqPiZY5xR5l.')) - - #test unicode password w/ high char - ret = safe_os_crypt(u'test\u1234', u'aa') - self.assertEqual(ret, (True, u'aahWwbrUsKZk.')) - - #test utf-8 password w/ high char - ret = safe_os_crypt(b('test\xe1\x88\xb4'), u'aa') - self.assertEqual(ret, (True, u'aahWwbrUsKZk.')) - - #test latin-1 password - ret = safe_os_crypt(b('test\xff'), u'aa') - # Py2k # - self.assertEqual(ret, (True, u'aaOx.5nbTU/.M')) - # Py3k # - #self.assertEqual(ret, (False, None)) - # end Py3k # - -#========================================================= -#byte/unicode helpers -#========================================================= + def test_crypt(self): + "test crypt.crypt() wrappers" + from passlib.utils import has_crypt, safe_crypt, test_crypt + + # test everything is disabled + if not has_crypt: + self.assertEqual(safe_crypt("test", "aa"), None) + self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) + raise self.skipTest("crypt.crypt() not available") + + # XXX: this assumes *every* crypt() implementation supports des_crypt. + # if this fails for some platform, this test will need modifying. + + # test return type + self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode) + + # test ascii password + h1 = u('aaqPiZY5xR5l.') + self.assertEqual(safe_crypt(u('test'), u('aa')), h1) + self.assertEqual(safe_crypt(b('test'), b('aa')), h1) + + # test utf-8 / unicode password + h2 = u('aahWwbrUsKZk.') + self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2) + self.assertEqual(safe_crypt(b('test\xe1\x88\xb4'), 'aa'), h2) + + # test latin-1 password + hash = safe_crypt(b('test\xff'), 'aa') + if PY3: # py3 supports utf-8 bytes only. + self.assertEqual(hash, None) + else: # but py2 is fine. + self.assertEqual(hash, u('aaOx.5nbTU/.M')) + + # test rejects null chars in password + self.assertRaises(ValueError, safe_crypt, '\x00', 'aa') + + # check test_crypt() + h1x = h1[:-1] + 'x' + self.assertTrue(test_crypt("test", h1)) + self.assertFalse(test_crypt("test", h1x)) + + # check crypt returning variant error indicators + # some platforms return None on errors, others empty string, + # The BSDs in some cases return ":" + import passlib.utils as mod + orig = mod._crypt + try: + fake = None + mod._crypt = lambda secret, hash: fake + for fake in [None, "", ":", ":0", "*0"]: + self.assertEqual(safe_crypt("test", "aa"), None) + self.assertFalse(test_crypt("test", h1)) + fake = 'xxx' + self.assertEqual(safe_crypt("test", "aa"), "xxx") + finally: + mod._crypt = orig + + def test_consteq(self): + "test consteq()" + # NOTE: this test is kind of over the top, but that's only because + # this is used for the critical task of comparing hashes for equality. + from passlib.utils import consteq + + # ensure error raises for wrong types + self.assertRaises(TypeError, consteq, u(''), b('')) + self.assertRaises(TypeError, consteq, u(''), 1) + self.assertRaises(TypeError, consteq, u(''), None) + + self.assertRaises(TypeError, consteq, b(''), u('')) + self.assertRaises(TypeError, consteq, b(''), 1) + self.assertRaises(TypeError, consteq, b(''), None) + + self.assertRaises(TypeError, consteq, None, u('')) + self.assertRaises(TypeError, consteq, None, b('')) + self.assertRaises(TypeError, consteq, 1, u('')) + self.assertRaises(TypeError, consteq, 1, b('')) + + # check equal inputs compare correctly + for value in [ + u("a"), + u("abc"), + u("\xff\xa2\x12\x00")*10, + ]: + self.assertTrue(consteq(value, value), "value %r:" % (value,)) + value = value.encode("latin-1") + self.assertTrue(consteq(value, value), "value %r:" % (value,)) + + # check non-equal inputs compare correctly + for l,r in [ + # check same-size comparisons with differing contents fail. + (u("a"), u("c")), + (u("abcabc"), u("zbaabc")), + (u("abcabc"), u("abzabc")), + (u("abcabc"), u("abcabz")), + ((u("\xff\xa2\x12\x00")*10)[:-1] + u("\x01"), + u("\xff\xa2\x12\x00")*10), + + # check different-size comparisons fail. + (u(""), u("a")), + (u("abc"), u("abcdef")), + (u("abc"), u("defabc")), + (u("qwertyuiopasdfghjklzxcvbnm"), u("abc")), + ]: + self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) + self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) + l = l.encode("latin-1") + r = r.encode("latin-1") + self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) + self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) + + # TODO: add some tests to ensure we take THETA(strlen) time. + # this might be hard to do reproducably. + # NOTE: below code was used to generate stats for analysis + ##from math import log as logb + ##import timeit + ##multipliers = [ 1< encode() -> decode() -> raw + # + + # generate some random bytes + size = random.randint(1 if saw_zero else 0, 12) + if not size: + saw_zero = True + enc_size = (4*size+2)//3 + raw = getrandbytes(random, size) + + # encode them, check invariants + encoded = engine.encode_bytes(raw) + self.assertEqual(len(encoded), enc_size) + + # make sure decode returns original + result = engine.decode_bytes(encoded) + self.assertEqual(result, raw) + + # + # test encoded -> decode() -> encode() -> encoded + # + + # generate some random encoded data + if size % 4 == 1: + size += random.choice([-1,1,2]) + raw_size = 3*size//4 + encoded = getrandstr(random, engine.bytemap, size) + + # decode them, check invariants + raw = engine.decode_bytes(encoded) + self.assertEqual(len(raw), raw_size, "encoded %d:" % size) + + # make sure encode returns original (barring padding bits) + result = engine.encode_bytes(raw) + if size % 4: + self.assertEqual(result[:-1], encoded[:-1]) + else: + self.assertEqual(result, encoded) + + def test_repair_unused(self): + "test repair_unused()" + # NOTE: this test relies on encode_bytes() always returning clear + # padding bits - which should be ensured by test vectors. + from passlib.utils import rng, getrandstr + engine = self.engine + check_repair_unused = self.engine.check_repair_unused + i = 0 + while i < 300: + size = rng.randint(0,23) + cdata = getrandstr(rng, engine.charmap, size).encode("ascii") + if size & 3 == 1: + # should throw error + self.assertRaises(ValueError, check_repair_unused, cdata) + continue + rdata = engine.encode_bytes(engine.decode_bytes(cdata)) + if rng.random() < .5: + cdata = cdata.decode("ascii") + rdata = rdata.decode("ascii") + if cdata == rdata: + # should leave unchanged + ok, result = check_repair_unused(cdata) + self.assertFalse(ok) + self.assertEqual(result, rdata) + else: + # should repair bits + self.assertNotEqual(size % 4, 0) + ok, result = check_repair_unused(cdata) + self.assertTrue(ok) + self.assertEqual(result, rdata) + i += 1 + + #=================================================================== + # test transposed encode/decode - encoding independant + #=================================================================== + # NOTE: these tests assume normal encode/decode has been tested elsewhere. + + transposed = [ + # orig, result, transpose map + (b("\x33\x22\x11"), b("\x11\x22\x33"),[2,1,0]), + (b("\x22\x33\x11"), b("\x11\x22\x33"),[1,2,0]), + ] + + transposed_dups = [ + # orig, result, transpose projection + (b("\x11\x11\x22"), b("\x11\x22\x33"),[0,0,1]), ] - def test_des_encrypt_block(self): - for k,p,c in self.test_des_vectors: - k = unhexlify(k) - p = unhexlify(p) - c = unhexlify(c) - result = des.des_encrypt_block(k,p) - self.assertEqual(result, c, "key=%r p=%r:" % (k,p)) - - #test 7 byte key - #FIXME: use a better key - k,p,c = b('00000000000000'), b('FFFFFFFFFFFFFFFF'), b('355550B2150E2451') - k = unhexlify(k) - p = unhexlify(p) - c = unhexlify(c) - result = des.des_encrypt_block(k,p) - self.assertEqual(result, c, "key=%r p=%r:" % (k,p)) - - def test_mdes_encrypt_int_block(self): - for k,p,c in self.test_des_vectors: - k = int(k,16) - p = int(p,16) - c = int(c,16) - result = des.mdes_encrypt_int_block(k,p, salt=0, rounds=1) - self.assertEqual(result, c, "key=%r p=%r:" % (k,p)) - - #TODO: test other des methods (eg: mdes_encrypt_int_block w/ salt & rounds) - # though des-crypt builtin backend test should thump it well enough - -#========================================================= -#hash64 -#========================================================= -class H64_Test(TestCase): + def test_encode_transposed_bytes(self): + "test encode_transposed_bytes()" + engine = self.engine + for result, input, offsets in self.transposed + self.transposed_dups: + tmp = engine.encode_transposed_bytes(input, offsets) + out = engine.decode_bytes(tmp) + self.assertEqual(out, result) + + self.assertRaises(TypeError, engine.encode_transposed_bytes, u("a"), []) + + def test_decode_transposed_bytes(self): + "test decode_transposed_bytes()" + engine = self.engine + for input, result, offsets in self.transposed: + tmp = engine.encode_bytes(input) + out = engine.decode_transposed_bytes(tmp, offsets) + self.assertEqual(out, result) + + def test_decode_transposed_bytes_bad(self): + "test decode_transposed_bytes() fails if map is a one-way" + engine = self.engine + for input, _, offsets in self.transposed_dups: + tmp = engine.encode_bytes(input) + self.assertRaises(TypeError, engine.decode_transposed_bytes, tmp, + offsets) + + #=================================================================== + # test 6bit handling + #=================================================================== + def check_int_pair(self, bits, encoded_pairs): + "helper to check encode_intXX & decode_intXX functions" + engine = self.engine + encode = getattr(engine, "encode_int%s" % bits) + decode = getattr(engine, "decode_int%s" % bits) + pad = -bits % 6 + chars = (bits+pad)//6 + upper = 1< 2 msb of last digit is padding - (b(".."), b("\x00")), # . = h64.CHARS[0b000000] - (b(".0"), b("\x80")), # 0 = h64.CHARS[0b000010] - (b(".2"), b("\x00")), # 2 = h64.CHARS[0b000100] - (b(".U"), b("\x00")), # U = h64.CHARS[0b100000] - - #len = 3 mod 4 -> 4 msb of last digit is padding - (b("..."), b("\x00\x00")), - (b("..6"), b("\x00\x80")), # 6 = h64.CHARS[0b001000] - (b("..E"), b("\x00\x00")), # E = h64.CHARS[0b010000] - (b("..U"), b("\x00\x00")), + encoded_ints = [ + (b("z."), 63, 12), + (b(".z"), 4032, 12), ] - def test_encode_bytes(self): - for source, result in self.encoded_bytes: - out = h64.encode_bytes(source) - self.assertEqual(out, result) - - def test_decode_bytes(self): - for result, source in self.encoded_bytes: - out = h64.decode_bytes(source) - self.assertEqual(out, result) - - #wrong size (1 % 4) - self.assertRaises(ValueError, h64.decode_bytes, b('abcde')) - - self.assertRaises(TypeError, h64.decode_bytes, u'abcd') - - def test_encode_int(self): - self.assertEqual(h64.encode_int(63, 11, True), b('..........z')) - self.assertEqual(h64.encode_int(63, 11), b('z..........')) - - self.assertRaises(ValueError, h64.encode_int64, -1) - - def test_decode_int(self): - self.assertEqual(h64.decode_int64(b('...........')), 0) - - self.assertRaises(ValueError, h64.decode_int12, b('a?')) - self.assertRaises(ValueError, h64.decode_int24, b('aaa?')) - self.assertRaises(ValueError, h64.decode_int64, b('aaa?aaa?aaa')) - self.assertRaises(ValueError, h64.decode_dc_int64, b('aaa?aaa?aaa')) - - self.assertRaises(TypeError, h64.decode_int12, u'a'*2) - self.assertRaises(TypeError, h64.decode_int24, u'a'*4) - self.assertRaises(TypeError, h64.decode_int64, u'a'*11) - self.assertRaises(TypeError, h64.decode_dc_int64, u'a'*11) - - def test_decode_bytes_padding(self): - for source, result in self.decode_padding_bytes: - out = h64.decode_bytes(source) - self.assertEqual(out, result) - self.assertRaises(TypeError, h64.decode_bytes, u'..') +class H64Big_Test(_Base64Test): + "test H64Big codec functions" + engine = h64big + descriptionPrefix = "h64big codec" - def test_decode_int6(self): - self.assertEqual(h64.decode_int6(b('.')),0) - self.assertEqual(h64.decode_int6(b('z')),63) - self.assertRaises(ValueError, h64.decode_int6, b('?')) - self.assertRaises(TypeError, h64.decode_int6, u'?') - - def test_encode_int6(self): - self.assertEqual(h64.encode_int6(0),b('.')) - self.assertEqual(h64.encode_int6(63),b('z')) - self.assertRaises(ValueError, h64.encode_int6, -1) - self.assertRaises(ValueError, h64.encode_int6, 64) - - #========================================================= - #test transposed encode/decode - #========================================================= - encode_transposed = [ - (b("\x33\x22\x11"), b("\x11\x22\x33"),[2,1,0]), - (b("\x22\x33\x11"), b("\x11\x22\x33"),[1,2,0]), - ] - - encode_transposed_dups = [ - (b("\x11\x11\x22"), b("\x11\x22\x33"),[0,0,1]), + encoded_data = [ + # test lengths 0..6 to ensure tail is encoded properly + (b(""),b("")), + (b("\x55"),b("JE")), + (b("\x55\xaa"),b("JOc")), + (b("\x55\xaa\x55"),b("JOdJ")), + (b("\x55\xaa\x55\xaa"),b("JOdJeU")), + (b("\x55\xaa\x55\xaa\x55"),b("JOdJeZI")), + (b("\x55\xaa\x55\xaa\x55\xaa"),b("JOdJeZKe")), + + # test padding bits are null + (b("\x55\xaa\x55\xaf"),b("JOdJfk")), # len = 1 mod 3 + (b("\x55\xaa\x55\xaa\x5f"),b("JOdJeZw")), # len = 2 mod 3 ] - def test_encode_transposed_bytes(self): - for result, input, offsets in self.encode_transposed + self.encode_transposed_dups: - tmp = h64.encode_transposed_bytes(input, offsets) - out = h64.decode_bytes(tmp) - self.assertEqual(out, result) - - def test_decode_transposed_bytes(self): - for input, result, offsets in self.encode_transposed: - tmp = h64.encode_bytes(input) - out = h64.decode_transposed_bytes(tmp, offsets) - self.assertEqual(out, result) - - def test_decode_transposed_bytes_bad(self): - for input, _, offsets in self.encode_transposed_dups: - tmp = h64.encode_bytes(input) - self.assertRaises(TypeError, h64.decode_transposed_bytes, tmp, offsets) - - #========================================================= - #TODO: test other h64 methods - #========================================================= - -#========================================================= -#test md4 -#========================================================= -class _MD4_Test(TestCase): - #test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5 - - hash = None - - vectors = [ - # input -> hex digest - (b(""), "31d6cfe0d16ae931b73c59d7e0c089c0"), - (b("a"), "bde52cb31de33e46245e05fbdbd6fb24"), - (b("abc"), "a448017aaf21d8525fc10ae87aa6729d"), - (b("message digest"), "d9130a8164549fe818874806e1c7014b"), - (b("abcdefghijklmnopqrstuvwxyz"), "d79e1c308aa5bbcdeea8ed63df412da9"), - (b("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), "043f8582f241db351ce627e153e7f0e4"), - (b("12345678901234567890123456789012345678901234567890123456789012345678901234567890"), "e33b4ddc9c38f2199c3e7b164fcc0536"), + encoded_ints = [ + (b(".z"), 63, 12), + (b("z."), 4032, 12), ] - def test_md4_update(self): - "test md4 update" - md4 = self.hash - h = md4(b('')) - self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") - - #NOTE: under py2, hashlib methods try to encode to ascii, - # though shouldn't rely on that. - # Py3k # - #self.assertRaises(TypeError, h.update, u'x') - # end Py3k # - - h.update(b('a')) - self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") - - h.update(b('bcdefghijklmnopqrstuvwxyz')) - self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9") - - def test_md4_hexdigest(self): - "test md4 hexdigest()" - md4 = self.hash - for input, hex in self.vectors: - out = md4(input).hexdigest() - self.assertEqual(out, hex) - - def test_md4_digest(self): - "test md4 digest()" - md4 = self.hash - for input, hex in self.vectors: - out = md4(input).digest() - self.assertEqual(to_native_str(hexlify(out)), hex) - - def test_md4_copy(self): - "test md4 copy()" - md4 = self.hash - h = md4(b('abc')) - - h2 = h.copy() - h2.update(b('def')) - self.assertEqual(h2.hexdigest(), '804e7f1c2586e50b49ac65db5b645131') - - h.update(b('ghi')) - self.assertEqual(h.hexdigest(), 'c5225580bfe176f6deeee33dee98732c') - -# -#now do a bunch of things to test multiple possible backends. -# - -has_ssl_md4 = (md4_mod.md4 is not md4_mod._builtin_md4) - -if has_ssl_md4: - class MD4_SSL_Test(_MD4_Test): - case_prefix = "MD4 (SSL version)" - hash = staticmethod(md4_mod.md4) - -if not has_ssl_md4 or enable_option("cover"): - class MD4_Builtin_Test(_MD4_Test): - case_prefix = "MD4 (builtin version)" - hash = md4_mod._builtin_md4 - -#========================================================= -#test passlib.utils.pbkdf2 -#========================================================= -import hashlib -import hmac -from passlib.utils import pbkdf2 - -#TODO: should we bother testing hmac_sha1() function? it's verified via sha1_crypt testing. - -class KdfTest(TestCase): - "test kdf helpers" - - def test_pbkdf1(self): - "test pbkdf1" - for secret, salt, rounds, klen, hash, correct in [ - #http://www.di-mgt.com.au/cryptoKDFs.html - (b('password'), hb('78578E5A5D63CB06'), 1000, 16, 'sha1', - hb('dc19847e05c64d2faf10ebfb4a3d2a20')), - ]: - result = pbkdf2.pbkdf1(secret, salt, rounds, klen, hash) - self.assertEqual(result, correct) - - #test rounds < 1 - #test klen < 0 - #test klen > block size - #test invalid hash - -#NOTE: this is not run directly, but via two subclasses (below) -class _Pbkdf2BackendTest(TestCase): - "test builtin unix crypt backend" - enable_m2crypto = False - - def setUp(self): - #disable m2crypto support so we'll always use software backend - if not self.enable_m2crypto: - self._orig_EVP = pbkdf2._EVP - pbkdf2._EVP = None - else: - #set flag so tests can check for m2crypto presence quickly - self.enable_m2crypto = bool(pbkdf2._EVP) - pbkdf2._clear_prf_cache() - - def tearDown(self): - if not self.enable_m2crypto: - pbkdf2._EVP = self._orig_EVP - pbkdf2._clear_prf_cache() - - #TODO: test get_prf() behavior in various situations - though overall behavior tested via pbkdf2 - - def test_rfc3962(self): - "rfc3962 test vectors" - self.assertFunctionResults(pbkdf2.pbkdf2, [ - # result, secret, salt, rounds, keylen, digest="sha1" - - #test case 1 / 128 bit - ( - hb("cdedb5281bb2f801565a1122b2563515"), - b("password"), b("ATHENA.MIT.EDUraeburn"), 1, 16 - ), - - #test case 2 / 128 bit - ( - hb("01dbee7f4a9e243e988b62c73cda935d"), - b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 16 - ), - - #test case 2 / 256 bit - ( - hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"), - b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 32 - ), - - #test case 3 / 256 bit - ( - hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"), - b("password"), b("ATHENA.MIT.EDUraeburn"), 1200, 32 - ), - - #test case 4 / 256 bit - ( - hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"), - b("password"), b('\x12\x34\x56\x78\x78\x56\x34\x12'), 5, 32 - ), - - #test case 5 / 256 bit - ( - hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"), - b("X"*64), b("pass phrase equals block size"), 1200, 32 - ), - - #test case 6 / 256 bit - ( - hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"), - b("X"*65), b("pass phrase exceeds block size"), 1200, 32 - ), - ]) - - def test_rfc6070(self): - "rfc6070 test vectors" - self.assertFunctionResults(pbkdf2.pbkdf2, [ - - ( - hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"), - b("password"), b("salt"), 1, 20, - ), - - ( - hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"), - b("password"), b("salt"), 2, 20, - ), - - ( - hb("4b007901b765489abead49d926f721d065a429c1"), - b("password"), b("salt"), 4096, 20, - ), - - #just runs too long - could enable if ALL option is set - ##( - ## - ## unhexlify("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"), - ## "password", "salt", 16777216, 20, - ##), - - ( - hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"), - b("passwordPASSWORDpassword"), - b("saltSALTsaltSALTsaltSALTsaltSALTsalt"), - 4096, 25, - ), - - ( - hb("56fa6aa75548099dcc37d7f03425e0c3"), - b("pass\00word"), b("sa\00lt"), 4096, 16, - ), - ]) - - def test_invalid_values(self): - - #invalid rounds - self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), -1, 16) - self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 0, 16) - self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), b('salt'), 'x', 16) - - #invalid keylen - self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 20*(2**32-1)+1) - - #invalid salt type - self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), 5, 1, 10) - - #invalid secret type - self.assertRaises(TypeError, pbkdf2.pbkdf2, 5, b('salt'), 1, 10) - - #invalid hash - self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 'hmac-foo') - self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 'foo') - self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 5) - - def test_hmac_sha1(self): - "test independant hmac_sha1() method" - self.assertEqual( - pbkdf2.hmac_sha1(b("secret"), b("salt")), - b('\xfc\xd4\x0c;]\r\x97\xc6\xf1S\x8d\x93\xb9\xeb\xc6\x00\x04.\x8b\xfe') - ) - - def test_sha1_string(self): - "test various prf values" - self.assertEqual( - pbkdf2.pbkdf2(b("secret"), b("salt"), 10, 16, "hmac-sha1"), - b('\xe2H\xfbk\x136QF\xf8\xacc\x07\xcc"(\x12') - ) - - def test_sha512_string(self): - "test alternate digest string (sha512)" - self.assertFunctionResults(pbkdf2.pbkdf2, [ - # result, secret, salt, rounds, keylen, digest="sha1" - - #case taken from example in http://grub.enbug.org/Authentication - ( - hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC6C29E293F0A0"), - b("hello"), - hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073994D79080136"), - 10000, 64, "hmac-sha512" - ), - ]) - - def test_sha512_function(self): - "test custom digest function" - def prf(key, msg): - return hmac.new(key, msg, hashlib.sha512).digest() - - self.assertFunctionResults(pbkdf2.pbkdf2, [ - # result, secret, salt, rounds, keylen, digest="sha1" - - #case taken from example in http://grub.enbug.org/Authentication - ( - hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC6C29E293F0A0"), - b("hello"), - hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073994D79080136"), - 10000, 64, prf, - ), - ]) - -has_m2crypto = (pbkdf2._EVP is not None) - -if has_m2crypto: - class Pbkdf2_M2Crypto_Test(_Pbkdf2BackendTest): - case_prefix = "pbkdf2 (m2crypto backend)" - enable_m2crypto = True - -if not has_m2crypto or enable_option("cover"): - class Pbkdf2_Builtin_Test(_Pbkdf2BackendTest): - case_prefix = "pbkdf2 (builtin backend)" - enable_m2crypto = False - -#========================================================= -#EOF -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_utils_crypto.py passlib-1.6.1/passlib/tests/test_utils_crypto.py --- passlib-1.5.3/passlib/tests/test_utils_crypto.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_utils_crypto.py 2012-08-01 17:10:03.000000000 +0000 @@ -0,0 +1,599 @@ +"""tests for passlib.utils.(des|pbkdf2|md4)""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +from binascii import hexlify, unhexlify +import hashlib +import hmac +import sys +import random +import warnings +# site +try: + import M2Crypto +except ImportError: + M2Crypto = None +# pkg +# module +from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \ + unicode, join_bytes, PYPY, JYTHON +from passlib.tests.utils import TestCase, TEST_MODE, catch_warnings, skipUnless, skipIf + +#============================================================================= +# support +#============================================================================= +def hb(source): + return unhexlify(b(source)) + +#============================================================================= +# test assorted crypto helpers +#============================================================================= +class CryptoTest(TestCase): + "test various crypto functions" + + ndn_formats = ["hashlib", "iana"] + ndn_values = [ + # (iana name, hashlib name, ... other unnormalized names) + ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"), + ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"), + ("sha256", "sha-256", "SHA_256", "sha2-256"), + ("ripemd", "ripemd", "SCRAM-RIPEMD", "RIPEMD"), + ("ripemd160", "ripemd-160", + "SCRAM-RIPEMD-160", "RIPEmd160"), + ("test128", "test-128", "TEST128"), + ("test2", "test2", "TEST-2"), + ("test3128", "test3-128", "TEST-3-128"), + ] + + def test_norm_hash_name(self): + "test norm_hash_name()" + from itertools import chain + from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names + + # test formats + for format in self.ndn_formats: + norm_hash_name("md4", format) + self.assertRaises(ValueError, norm_hash_name, "md4", None) + self.assertRaises(ValueError, norm_hash_name, "md4", "fake") + + # test types + self.assertEqual(norm_hash_name(u("MD4")), "md4") + self.assertEqual(norm_hash_name(b("MD4")), "md4") + self.assertRaises(TypeError, norm_hash_name, None) + + # test selected results + with catch_warnings(): + warnings.filterwarnings("ignore", '.*unknown hash') + for row in chain(_nhn_hash_names, self.ndn_values): + for idx, format in enumerate(self.ndn_formats): + correct = row[idx] + for value in row: + result = norm_hash_name(value, format) + self.assertEqual(result, correct, + "name=%r, format=%r:" % (value, + format)) + + # TODO: write full test of get_prf(), currently relying on pbkdf2 testing + +#============================================================================= +# test DES routines +#============================================================================= +class DesTest(TestCase): + descriptionPrefix = "DES" + + # test vectors taken from http://www.skepticfiles.org/faq/testdes.htm + des_test_vectors = [ + # key, plaintext, ciphertext + (0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7), + (0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0x7359B2163E4EDC58), + (0x3000000000000000, 0x1000000000000001, 0x958E6E627A05557B), + (0x1111111111111111, 0x1111111111111111, 0xF40379AB9E0EC533), + (0x0123456789ABCDEF, 0x1111111111111111, 0x17668DFC7292532D), + (0x1111111111111111, 0x0123456789ABCDEF, 0x8A5AE1F81AB8F2DD), + (0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7), + (0xFEDCBA9876543210, 0x0123456789ABCDEF, 0xED39D950FA74BCC4), + (0x7CA110454A1A6E57, 0x01A1D6D039776742, 0x690F5B0D9A26939B), + (0x0131D9619DC1376E, 0x5CD54CA83DEF57DA, 0x7A389D10354BD271), + (0x07A1133E4A0B2686, 0x0248D43806F67172, 0x868EBB51CAB4599A), + (0x3849674C2602319E, 0x51454B582DDF440A, 0x7178876E01F19B2A), + (0x04B915BA43FEB5B6, 0x42FD443059577FA2, 0xAF37FB421F8C4095), + (0x0113B970FD34F2CE, 0x059B5E0851CF143A, 0x86A560F10EC6D85B), + (0x0170F175468FB5E6, 0x0756D8E0774761D2, 0x0CD3DA020021DC09), + (0x43297FAD38E373FE, 0x762514B829BF486A, 0xEA676B2CB7DB2B7A), + (0x07A7137045DA2A16, 0x3BDD119049372802, 0xDFD64A815CAF1A0F), + (0x04689104C2FD3B2F, 0x26955F6835AF609A, 0x5C513C9C4886C088), + (0x37D06BB516CB7546, 0x164D5E404F275232, 0x0A2AEEAE3FF4AB77), + (0x1F08260D1AC2465E, 0x6B056E18759F5CCA, 0xEF1BF03E5DFA575A), + (0x584023641ABA6176, 0x004BD6EF09176062, 0x88BF0DB6D70DEE56), + (0x025816164629B007, 0x480D39006EE762F2, 0xA1F9915541020B56), + (0x49793EBC79B3258F, 0x437540C8698F3CFA, 0x6FBF1CAFCFFD0556), + (0x4FB05E1515AB73A7, 0x072D43A077075292, 0x2F22E49BAB7CA1AC), + (0x49E95D6D4CA229BF, 0x02FE55778117F12A, 0x5A6B612CC26CCE4A), + (0x018310DC409B26D6, 0x1D9D5C5018F728C2, 0x5F4C038ED12B2E41), + (0x1C587F1C13924FEF, 0x305532286D6F295A, 0x63FAC0D034D9F793), + (0x0101010101010101, 0x0123456789ABCDEF, 0x617B3A0CE8F07100), + (0x1F1F1F1F0E0E0E0E, 0x0123456789ABCDEF, 0xDB958605F8C8C606), + (0xE0FEE0FEF1FEF1FE, 0x0123456789ABCDEF, 0xEDBFD1C66C29CCC7), + (0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x355550B2150E2451), + (0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAAAAF4DEAF1DBAE), + (0x0123456789ABCDEF, 0x0000000000000000, 0xD5D44FF720683D0D), + (0xFEDCBA9876543210, 0xFFFFFFFFFFFFFFFF, 0x2A2BB008DF97C2F2), + ] + + def test_01_expand(self): + "test expand_des_key()" + from passlib.utils.des import expand_des_key, shrink_des_key, \ + _KDATA_MASK, INT_56_MASK + + # make sure test vectors are preserved (sans parity bits) + # uses ints, bytes are tested under # 02 + for key1, _, _ in self.des_test_vectors: + key2 = shrink_des_key(key1) + key3 = expand_des_key(key2) + # NOTE: this assumes expand_des_key() sets parity bits to 0 + self.assertEqual(key3, key1 & _KDATA_MASK) + + # type checks + self.assertRaises(TypeError, expand_des_key, 1.0) + + # too large + self.assertRaises(ValueError, expand_des_key, INT_56_MASK+1) + self.assertRaises(ValueError, expand_des_key, b("\x00")*8) + + # too small + self.assertRaises(ValueError, expand_des_key, -1) + self.assertRaises(ValueError, expand_des_key, b("\x00")*6) + + def test_02_shrink(self): + "test shrink_des_key()" + from passlib.utils.des import expand_des_key, shrink_des_key, \ + INT_64_MASK + from passlib.utils import random, getrandbytes + + # make sure reverse works for some random keys + # uses bytes, ints are tested under # 01 + for i in range(20): + key1 = getrandbytes(random, 7) + key2 = expand_des_key(key1) + key3 = shrink_des_key(key2) + self.assertEqual(key3, key1) + + # type checks + self.assertRaises(TypeError, shrink_des_key, 1.0) + + # too large + self.assertRaises(ValueError, shrink_des_key, INT_64_MASK+1) + self.assertRaises(ValueError, shrink_des_key, b("\x00")*9) + + # too small + self.assertRaises(ValueError, shrink_des_key, -1) + self.assertRaises(ValueError, shrink_des_key, b("\x00")*7) + + def _random_parity(self, key): + "randomize parity bits" + from passlib.utils.des import _KDATA_MASK, _KPARITY_MASK, INT_64_MASK + from passlib.utils import rng + return (key & _KDATA_MASK) | (rng.randint(0,INT_64_MASK) & _KPARITY_MASK) + + def test_03_encrypt_bytes(self): + "test des_encrypt_block()" + from passlib.utils.des import (des_encrypt_block, shrink_des_key, + _pack64, _unpack64) + + # run through test vectors + for key, plaintext, correct in self.des_test_vectors: + # convert to bytes + key = _pack64(key) + plaintext = _pack64(plaintext) + correct = _pack64(correct) + + # test 64-bit key + result = des_encrypt_block(key, plaintext) + self.assertEqual(result, correct, "key=%r plaintext=%r:" % + (key, plaintext)) + + # test 56-bit version + key2 = shrink_des_key(key) + result = des_encrypt_block(key2, plaintext) + self.assertEqual(result, correct, "key=%r shrink(key)=%r plaintext=%r:" % + (key, key2, plaintext)) + + # test with random parity bits + for _ in range(20): + key3 = _pack64(self._random_parity(_unpack64(key))) + result = des_encrypt_block(key3, plaintext) + self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" % + (key, key3, plaintext)) + + # check invalid keys + stub = b('\x00') * 8 + self.assertRaises(TypeError, des_encrypt_block, 0, stub) + self.assertRaises(ValueError, des_encrypt_block, b('\x00')*6, stub) + + # check invalid input + self.assertRaises(TypeError, des_encrypt_block, stub, 0) + self.assertRaises(ValueError, des_encrypt_block, stub, b('\x00')*7) + + # check invalid salts + self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=-1) + self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=1<<24) + + # check invalid rounds + self.assertRaises(ValueError, des_encrypt_block, stub, stub, 0, rounds=0) + + def test_04_encrypt_ints(self): + "test des_encrypt_int_block()" + from passlib.utils.des import (des_encrypt_int_block, shrink_des_key) + + # run through test vectors + for key, plaintext, correct in self.des_test_vectors: + # test 64-bit key + result = des_encrypt_int_block(key, plaintext) + self.assertEqual(result, correct, "key=%r plaintext=%r:" % + (key, plaintext)) + + # test with random parity bits + for _ in range(20): + key3 = self._random_parity(key) + result = des_encrypt_int_block(key3, plaintext) + self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" % + (key, key3, plaintext)) + + # check invalid keys + self.assertRaises(TypeError, des_encrypt_int_block, b('\x00'), 0) + self.assertRaises(ValueError, des_encrypt_int_block, -1, 0) + + # check invalid input + self.assertRaises(TypeError, des_encrypt_int_block, 0, b('\x00')) + self.assertRaises(ValueError, des_encrypt_int_block, 0, -1) + + # check invalid salts + self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=-1) + self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=1<<24) + + # check invalid rounds + self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, 0, rounds=0) + +#============================================================================= +# test pure-python MD4 implementation +#============================================================================= +from passlib.utils.md4 import _has_native_md4 +has_native_md4 = _has_native_md4() + +class _MD4_Test(TestCase): + _disable_native = False + + def setUp(self): + super(_MD4_Test, self).setUp() + import passlib.utils.md4 as mod + if has_native_md4 and self._disable_native: + self.addCleanup(setattr, mod, "md4", mod.md4) + mod.md4 = mod._builtin_md4 + + vectors = [ + # input -> hex digest + # test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5 + (b(""), "31d6cfe0d16ae931b73c59d7e0c089c0"), + (b("a"), "bde52cb31de33e46245e05fbdbd6fb24"), + (b("abc"), "a448017aaf21d8525fc10ae87aa6729d"), + (b("message digest"), "d9130a8164549fe818874806e1c7014b"), + (b("abcdefghijklmnopqrstuvwxyz"), "d79e1c308aa5bbcdeea8ed63df412da9"), + (b("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), "043f8582f241db351ce627e153e7f0e4"), + (b("12345678901234567890123456789012345678901234567890123456789012345678901234567890"), "e33b4ddc9c38f2199c3e7b164fcc0536"), + ] + + def test_md4_update(self): + "test md4 update" + from passlib.utils.md4 import md4 + h = md4(b('')) + self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") + + # NOTE: under py2, hashlib methods try to encode to ascii, + # though shouldn't rely on that. + if PY3 or self._disable_native: + self.assertRaises(TypeError, h.update, u('x')) + + h.update(b('a')) + self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") + + h.update(b('bcdefghijklmnopqrstuvwxyz')) + self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9") + + def test_md4_hexdigest(self): + "test md4 hexdigest()" + from passlib.utils.md4 import md4 + for input, hex in self.vectors: + out = md4(input).hexdigest() + self.assertEqual(out, hex) + + def test_md4_digest(self): + "test md4 digest()" + from passlib.utils.md4 import md4 + for input, hex in self.vectors: + out = bascii_to_str(hexlify(md4(input).digest())) + self.assertEqual(out, hex) + + def test_md4_copy(self): + "test md4 copy()" + from passlib.utils.md4 import md4 + h = md4(b('abc')) + + h2 = h.copy() + h2.update(b('def')) + self.assertEqual(h2.hexdigest(), '804e7f1c2586e50b49ac65db5b645131') + + h.update(b('ghi')) + self.assertEqual(h.hexdigest(), 'c5225580bfe176f6deeee33dee98732c') + +# create subclasses to test with and without native backend +class MD4_SSL_Test(_MD4_Test): + descriptionPrefix = "MD4 (ssl version)" +MD4_SSL_TEST = skipUnless(has_native_md4, "hashlib lacks ssl support")(MD4_SSL_Test) + +class MD4_Builtin_Test(_MD4_Test): + descriptionPrefix = "MD4 (builtin version)" + _disable_native = True +MD4_Builtin_Test = skipUnless(TEST_MODE("full") or not has_native_md4, + "skipped under current test mode")(MD4_Builtin_Test) + +#============================================================================= +# test PBKDF1 support +#============================================================================= +class Pbkdf1_Test(TestCase): + "test kdf helpers" + descriptionPrefix = "pbkdf1" + + pbkdf1_tests = [ + # (password, salt, rounds, keylen, hash, result) + + # + # from http://www.di-mgt.com.au/cryptoKDFs.html + # + (b('password'), hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')), + + # + # custom + # + (b('password'), b('salt'), 1000, 0, 'md5', b('')), + (b('password'), b('salt'), 1000, 1, 'md5', hb('84')), + (b('password'), b('salt'), 1000, 8, 'md5', hb('8475c6a8531a5d27')), + (b('password'), b('salt'), 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')), + (b('password'), b('salt'), 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')), + (b('password'), b('salt'), 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')), + ] + if not (PYPY or JYTHON): + pbkdf1_tests.append( + (b('password'), b('salt'), 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453')) + ) + + def test_known(self): + "test reference vectors" + from passlib.utils.pbkdf2 import pbkdf1 + for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests: + result = pbkdf1(secret, salt, rounds, keylen, digest) + self.assertEqual(result, correct) + + def test_border(self): + "test border cases" + from passlib.utils.pbkdf2 import pbkdf1 + def helper(secret=b('secret'), salt=b('salt'), rounds=1, keylen=1, hash='md5'): + return pbkdf1(secret, salt, rounds, keylen, hash) + helper() + + # salt/secret wrong type + self.assertRaises(TypeError, helper, secret=1) + self.assertRaises(TypeError, helper, salt=1) + + # non-existent hashes + self.assertRaises(ValueError, helper, hash='missing') + + # rounds < 1 and wrong type + self.assertRaises(ValueError, helper, rounds=0) + self.assertRaises(TypeError, helper, rounds='1') + + # keylen < 0, keylen > block_size, and wrong type + self.assertRaises(ValueError, helper, keylen=-1) + self.assertRaises(ValueError, helper, keylen=17, hash='md5') + self.assertRaises(TypeError, helper, keylen='1') + +#============================================================================= +# test PBKDF2 support +#============================================================================= +class _Pbkdf2_Test(TestCase): + "test pbkdf2() support" + _disable_m2crypto = False + + def setUp(self): + super(_Pbkdf2_Test, self).setUp() + import passlib.utils.pbkdf2 as mod + + # disable m2crypto support, and use software backend + if M2Crypto and self._disable_m2crypto: + self.addCleanup(setattr, mod, "_EVP", mod._EVP) + mod._EVP = None + + # flush cached prf functions, since we're screwing with their backend. + mod._clear_prf_cache() + self.addCleanup(mod._clear_prf_cache) + + pbkdf2_test_vectors = [ + # (result, secret, salt, rounds, keylen, prf="sha1") + + # + # from rfc 3962 + # + + # test case 1 / 128 bit + ( + hb("cdedb5281bb2f801565a1122b2563515"), + b("password"), b("ATHENA.MIT.EDUraeburn"), 1, 16 + ), + + # test case 2 / 128 bit + ( + hb("01dbee7f4a9e243e988b62c73cda935d"), + b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 16 + ), + + # test case 2 / 256 bit + ( + hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"), + b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 32 + ), + + # test case 3 / 256 bit + ( + hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"), + b("password"), b("ATHENA.MIT.EDUraeburn"), 1200, 32 + ), + + # test case 4 / 256 bit + ( + hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"), + b("password"), b('\x12\x34\x56\x78\x78\x56\x34\x12'), 5, 32 + ), + + # test case 5 / 256 bit + ( + hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"), + b("X"*64), b("pass phrase equals block size"), 1200, 32 + ), + + # test case 6 / 256 bit + ( + hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"), + b("X"*65), b("pass phrase exceeds block size"), 1200, 32 + ), + + # + # from rfc 6070 + # + ( + hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"), + b("password"), b("salt"), 1, 20, + ), + + ( + hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"), + b("password"), b("salt"), 2, 20, + ), + + ( + hb("4b007901b765489abead49d926f721d065a429c1"), + b("password"), b("salt"), 4096, 20, + ), + + # just runs too long - could enable if ALL option is set + ##( + ## + ## unhexlify("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"), + ## "password", "salt", 16777216, 20, + ##), + + ( + hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"), + b("passwordPASSWORDpassword"), + b("saltSALTsaltSALTsaltSALTsaltSALTsalt"), + 4096, 25, + ), + + ( + hb("56fa6aa75548099dcc37d7f03425e0c3"), + b("pass\00word"), b("sa\00lt"), 4096, 16, + ), + + # + # from example in http://grub.enbug.org/Authentication + # + ( + hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED" + "97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC" + "6C29E293F0A0"), + b("hello"), + hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71" + "784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073" + "994D79080136"), + 10000, 64, "hmac-sha512" + ), + + # + # custom + # + ( + hb('e248fb6b13365146f8ac6307cc222812'), + b("secret"), b("salt"), 10, 16, "hmac-sha1", + ), + ( + hb('e248fb6b13365146f8ac6307cc2228127872da6d'), + b("secret"), b("salt"), 10, None, "hmac-sha1", + ), + + ] + + def test_known(self): + "test reference vectors" + from passlib.utils.pbkdf2 import pbkdf2 + for row in self.pbkdf2_test_vectors: + correct, secret, salt, rounds, keylen = row[:5] + prf = row[5] if len(row) == 6 else "hmac-sha1" + result = pbkdf2(secret, salt, rounds, keylen, prf) + self.assertEqual(result, correct) + + def test_border(self): + "test border cases" + from passlib.utils.pbkdf2 import pbkdf2 + def helper(secret=b('password'), salt=b('salt'), rounds=1, keylen=None, prf="hmac-sha1"): + return pbkdf2(secret, salt, rounds, keylen, prf) + helper() + + # invalid rounds + self.assertRaises(ValueError, helper, rounds=0) + self.assertRaises(TypeError, helper, rounds='x') + + # invalid keylen + helper(keylen=0) + self.assertRaises(ValueError, helper, keylen=-1) + self.assertRaises(ValueError, helper, keylen=20*(2**32-1)+1) + self.assertRaises(TypeError, helper, keylen='x') + + # invalid secret/salt type + self.assertRaises(TypeError, helper, salt=5) + self.assertRaises(TypeError, helper, secret=5) + + # invalid hash + self.assertRaises(ValueError, helper, prf='hmac-foo') + self.assertRaises(ValueError, helper, prf='foo') + self.assertRaises(TypeError, helper, prf=5) + + def test_default_keylen(self): + "test keylen==None" + from passlib.utils.pbkdf2 import pbkdf2 + def helper(secret=b('password'), salt=b('salt'), rounds=1, keylen=None, prf="hmac-sha1"): + return pbkdf2(secret, salt, rounds, keylen, prf) + self.assertEqual(len(helper(prf='hmac-sha1')), 20) + self.assertEqual(len(helper(prf='hmac-sha256')), 32) + + def test_custom_prf(self): + "test custom prf function" + from passlib.utils.pbkdf2 import pbkdf2 + def prf(key, msg): + return hashlib.md5(key+msg+b('fooey')).digest() + result = pbkdf2(b('secret'), b('salt'), 1000, 20, prf) + self.assertEqual(result, hb('5fe7ce9f7e379d3f65cbc66ba8aa6440474a6849')) + +# create subclasses to test with and without m2crypto +class Pbkdf2_M2Crypto_Test(_Pbkdf2_Test): + descriptionPrefix = "pbkdf2 (m2crypto backend)" +Pbkdf2_M2Crypto_Test = skipUnless(M2Crypto, "M2Crypto not found")(Pbkdf2_M2Crypto_Test) + +class Pbkdf2_Builtin_Test(_Pbkdf2_Test): + descriptionPrefix = "pbkdf2 (builtin backend)" + _disable_m2crypto = True +Pbkdf2_Builtin_Test = skipUnless(TEST_MODE("full") or not M2Crypto, + "skipped under current test mode")(Pbkdf2_Builtin_Test) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_utils_handlers.py passlib-1.6.1/passlib/tests/test_utils_handlers.py --- passlib-1.5.3/passlib/tests/test_utils_handlers.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_utils_handlers.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,168 +1,319 @@ """tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import re import hashlib from logging import getLogger import warnings -#site -#pkg -from passlib.hash import ldap_md5 +# site +# pkg +from passlib.hash import ldap_md5, sha256_crypt from passlib.registry import _unload_handler_name as unload_handler_name, \ register_crypt_handler, get_crypt_handler -from passlib.utils import rng, getrandstr, handlers as uh, bytes, b, \ - to_hash_str, to_unicode, MissingBackendError, jython_vm -from passlib.tests.utils import HandlerCase, TestCase, catch_warnings, \ - dummy_handler_in_registry -#module +from passlib.exc import MissingBackendError, PasslibHashWarning +from passlib.utils import getrandstr, JYTHON, rng +from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \ + uascii_to_str, unicode, PY_MAX_25, SUPPORTS_DIR_METHOD +import passlib.utils.handlers as uh +from passlib.tests.utils import HandlerCase, TestCase, catch_warnings +from passlib.utils.compat import u, PY3 +# module log = getLogger(__name__) -#========================================================= -#test support classes - StaticHandler, GenericHandler, etc -#========================================================= +#============================================================================= +# utils +#============================================================================= +def _makelang(alphabet, size): + "generate all strings of given size using alphabet" + def helper(size): + if size < 2: + for char in alphabet: + yield char + else: + for char in alphabet: + for tail in helper(size-1): + yield char+tail + return set(helper(size)) + +#============================================================================= +# test GenericHandler & associates mixin classes +#============================================================================= class SkeletonTest(TestCase): "test hash support classes" - #========================================================= - #StaticHandler - #========================================================= + #=================================================================== + # StaticHandler + #=================================================================== def test_00_static_handler(self): - "test StaticHandler helper class" + "test StaticHandler class" class d1(uh.StaticHandler): name = "d1" context_kwds = ("flag",) + _hash_prefix = u("_") + checksum_chars = u("ab") + checksum_size = 1 + + def __init__(self, flag=False, **kwds): + super(d1, self).__init__(**kwds) + self.flag = flag + + def _calc_checksum(self, secret): + return u('b') if self.flag else u('a') + + # check default identify method + self.assertTrue(d1.identify(u('_a'))) + self.assertTrue(d1.identify(b('_a'))) + self.assertTrue(d1.identify(u('_b'))) + + self.assertFalse(d1.identify(u('_c'))) + self.assertFalse(d1.identify(b('_c'))) + self.assertFalse(d1.identify(u('a'))) + self.assertFalse(d1.identify(u('b'))) + self.assertFalse(d1.identify(u('c'))) + self.assertRaises(TypeError, d1.identify, None) + self.assertRaises(TypeError, d1.identify, 1) - @classmethod - def genhash(cls, secret, hash, flag=False): - if isinstance(hash, bytes): - hash = hash.decode("ascii") - if hash not in (u'a',u'b'): - raise ValueError - return to_hash_str(u'b' if flag else u'a') + # check default genconfig method + self.assertIs(d1.genconfig(), None) - #check default identify method - self.assertTrue(d1.identify(u'a')) - self.assertTrue(d1.identify(b('a'))) - self.assertTrue(d1.identify(u'b')) - self.assertFalse(d1.identify(u'c')) - self.assertFalse(d1.identify(b('c'))) - self.assertFalse(d1.identify(u'')) - self.assertFalse(d1.identify(None)) + # check default verify method + self.assertTrue(d1.verify('s', b('_a'))) + self.assertTrue(d1.verify('s',u('_a'))) + self.assertFalse(d1.verify('s', b('_b'))) + self.assertFalse(d1.verify('s',u('_b'))) + self.assertTrue(d1.verify('s', b('_b'), flag=True)) + self.assertRaises(ValueError, d1.verify, 's', b('_c')) + self.assertRaises(ValueError, d1.verify, 's', u('_c')) + + # check default encrypt method + self.assertEqual(d1.encrypt('s'), '_a') + self.assertEqual(d1.encrypt('s', flag=True), '_b') + + def test_01_calc_checksum_hack(self): + "test StaticHandler legacy attr" + # release 1.5 StaticHandler required genhash(), + # not _calc_checksum, be implemented. we have backward compat wrapper, + # this tests that it works. - #check default genconfig method - self.assertIs(d1.genconfig(), None) - d1._stub_config = u'b' - self.assertEqual(d1.genconfig(), to_hash_str('b')) + class d1(uh.StaticHandler): + name = "d1" + + @classmethod + def identify(self, hash): + if not hash or len(hash) != 40: + return False + try: + int(hash, 16) + except ValueError: + return False + return True - #check default verify method - self.assertTrue(d1.verify('s','a')) - self.assertTrue(d1.verify('s',u'a')) - self.assertFalse(d1.verify('s','b')) - self.assertFalse(d1.verify('s',u'b')) - self.assertTrue(d1.verify('s', 'b', flag=True)) - self.assertRaises(ValueError, d1.verify, 's', 'c') - - #check default encrypt method - self.assertEqual(d1.encrypt('s'), to_hash_str('a')) - self.assertEqual(d1.encrypt('s'), to_hash_str('a')) - self.assertEqual(d1.encrypt('s', flag=True), to_hash_str('b')) - - #========================================================= - #GenericHandler & mixins - #========================================================= + @classmethod + def genhash(cls, secret, hash): + if secret is None: + raise TypeError("no secret provided") + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if hash is not None and not cls.identify(hash): + raise ValueError("invalid hash") + return hashlib.sha1(b("xyz") + secret).hexdigest() + + @classmethod + def verify(cls, secret, hash): + if hash is None: + raise ValueError("no hash specified") + return cls.genhash(secret, hash) == hash.lower() + + # encrypt should issue api warnings, but everything else should be fine. + with self.assertWarningList("d1.*should be updated.*_calc_checksum"): + hash = d1.encrypt("test") + self.assertEqual(hash, '7c622762588a0e5cc786ad0a143156f9fd38eea3') + + self.assertTrue(d1.verify("test", hash)) + self.assertFalse(d1.verify("xtest", hash)) + + # not defining genhash either, however, should cause NotImplementedError + del d1.genhash + self.assertRaises(NotImplementedError, d1.encrypt, 'test') + + #=================================================================== + # GenericHandler & mixins + #=================================================================== def test_10_identify(self): "test GenericHandler.identify()" class d1(uh.GenericHandler): @classmethod def from_string(cls, hash): - if hash == 'a': - return cls(checksum='a') + if isinstance(hash, bytes): + hash = hash.decode("ascii") + if hash == u('a'): + return cls(checksum=hash) else: raise ValueError - #check fallback - self.assertFalse(d1.identify(None)) + # check fallback + self.assertRaises(TypeError, d1.identify, None) + self.assertRaises(TypeError, d1.identify, 1) self.assertFalse(d1.identify('')) self.assertTrue(d1.identify('a')) self.assertFalse(d1.identify('b')) - #check ident-based - d1.ident = u'!' - self.assertFalse(d1.identify(None)) - self.assertFalse(d1.identify('')) + # check regexp + d1._hash_regex = re.compile(u('@.')) + self.assertRaises(TypeError, d1.identify, None) + self.assertRaises(TypeError, d1.identify, 1) + self.assertTrue(d1.identify('@a')) + self.assertFalse(d1.identify('a')) + del d1._hash_regex + + # check ident-based + d1.ident = u('!') + self.assertRaises(TypeError, d1.identify, None) + self.assertRaises(TypeError, d1.identify, 1) self.assertTrue(d1.identify('!a')) self.assertFalse(d1.identify('a')) + del d1.ident def test_11_norm_checksum(self): - "test GenericHandler.norm_checksum()" + "test GenericHandler checksum handling" + # setup helpers class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 - checksum_chars = 'x' - self.assertRaises(ValueError, d1.norm_checksum, 'xxx') - self.assertEqual(d1.norm_checksum('xxxx'), 'xxxx') - self.assertRaises(ValueError, d1.norm_checksum, 'xxxxx') - self.assertRaises(ValueError, d1.norm_checksum, 'xxyx') + checksum_chars = u('xz') + _stub_checksum = u('z')*4 - def test_20_norm_salt(self): - "test GenericHandler+HasSalt: .norm_salt(), .generate_salt()" - class d1(uh.HasSalt, uh.GenericHandler): + def norm_checksum(*a, **k): + return d1(*a, **k).checksum + + # too small + self.assertRaises(ValueError, norm_checksum, u('xxx')) + + # right size + self.assertEqual(norm_checksum(u('xxxx')), u('xxxx')) + self.assertEqual(norm_checksum(u('xzxz')), u('xzxz')) + + # too large + self.assertRaises(ValueError, norm_checksum, u('xxxxx')) + + # wrong chars + self.assertRaises(ValueError, norm_checksum, u('xxyx')) + + # wrong type + self.assertRaises(TypeError, norm_checksum, b('xxyx')) + + # relaxed + with self.assertWarningList("checksum should be unicode"): + self.assertEqual(norm_checksum(b('xxzx'), relaxed=True), u('xxzx')) + self.assertRaises(TypeError, norm_checksum, 1, relaxed=True) + + # test _stub_checksum behavior + self.assertIs(norm_checksum(u('zzzz')), None) + + def test_12_norm_checksum_raw(self): + "test GenericHandler + HasRawChecksum mixin" + class d1(uh.HasRawChecksum, uh.GenericHandler): name = 'd1' - setting_kwds = ('salt',) - min_salt_size = 1 - max_salt_size = 3 - default_salt_size = 2 - salt_chars = 'a' - - #check salt=None - self.assertEqual(d1.norm_salt(None), 'aa') - self.assertRaises(ValueError, d1.norm_salt, None, strict=True) - - #check small & large salts - with catch_warnings(): - warnings.filterwarnings("ignore", ".* salt string must be at (least|most) .*", UserWarning) - self.assertEqual(d1.norm_salt('aaaa'), 'aaa') - self.assertRaises(ValueError, d1.norm_salt, '') - self.assertRaises(ValueError, d1.norm_salt, 'aaaa', strict=True) - - #check generate salt (indirectly) - self.assertEqual(len(d1.norm_salt(None)), 2) - self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1) - self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3) - self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 3) - self.assertRaises(ValueError, d1.norm_salt, None, salt_size=5, strict=True) + checksum_size = 4 + _stub_checksum = b('0')*4 + + def norm_checksum(*a, **k): + return d1(*a, **k).checksum + + # test bytes + self.assertEqual(norm_checksum(b('1234')), b('1234')) - def test_21_norm_salt(self): - "test GenericHandler+HasSalt: .norm_salt(), .generate_salt() - with no max_salt_size" + # test unicode + self.assertRaises(TypeError, norm_checksum, u('xxyx')) + self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True) + + # test _stub_checksum behavior + self.assertIs(norm_checksum(b('0')*4), None) + + def test_20_norm_salt(self): + "test GenericHandler + HasSalt mixin" + # setup helpers class d1(uh.HasSalt, uh.GenericHandler): name = 'd1' setting_kwds = ('salt',) - min_salt_size = 1 - max_salt_size = None - default_salt_size = 2 - salt_chars = 'a' - - #check salt=None - self.assertEqual(d1.norm_salt(None), 'aa') - self.assertRaises(ValueError, d1.norm_salt, None, strict=True) - - #check small & large salts - self.assertRaises(ValueError, d1.norm_salt, '') - self.assertEqual(d1.norm_salt('aaaa', strict=True), 'aaaa') - - #check generate salt (indirectly) - self.assertEqual(len(d1.norm_salt(None)), 2) - self.assertEqual(len(d1.norm_salt(None,salt_size=1)), 1) - self.assertEqual(len(d1.norm_salt(None,salt_size=3)), 3) - self.assertEqual(len(d1.norm_salt(None,salt_size=5)), 5) + min_salt_size = 2 + max_salt_size = 4 + default_salt_size = 3 + salt_chars = 'ab' + + def norm_salt(**k): + return d1(**k).salt + + def gen_salt(sz, **k): + return d1(use_defaults=True, salt_size=sz, **k).salt + + salts2 = _makelang('ab', 2) + salts3 = _makelang('ab', 3) + salts4 = _makelang('ab', 4) + + # check salt=None + self.assertRaises(TypeError, norm_salt) + self.assertRaises(TypeError, norm_salt, salt=None) + self.assertIn(norm_salt(use_defaults=True), salts3) + + # check explicit salts + with catch_warnings(record=True) as wlog: + + # check too-small salts + self.assertRaises(ValueError, norm_salt, salt='') + self.assertRaises(ValueError, norm_salt, salt='a') + self.consumeWarningList(wlog) + + # check correct salts + self.assertEqual(norm_salt(salt='ab'), 'ab') + self.assertEqual(norm_salt(salt='aba'), 'aba') + self.assertEqual(norm_salt(salt='abba'), 'abba') + self.consumeWarningList(wlog) + + # check too-large salts + self.assertRaises(ValueError, norm_salt, salt='aaaabb') + self.consumeWarningList(wlog) + + self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa') + self.consumeWarningList(wlog, PasslibHashWarning) + + # check generated salts + with catch_warnings(record=True) as wlog: + + # check too-small salt size + self.assertRaises(ValueError, gen_salt, 0) + self.assertRaises(ValueError, gen_salt, 1) + self.consumeWarningList(wlog) + + # check correct salt size + self.assertIn(gen_salt(2), salts2) + self.assertIn(gen_salt(3), salts3) + self.assertIn(gen_salt(4), salts4) + self.consumeWarningList(wlog) + + # check too-large salt size + self.assertRaises(ValueError, gen_salt, 5) + self.consumeWarningList(wlog) + + self.assertIn(gen_salt(5, relaxed=True), salts4) + self.consumeWarningList(wlog, ["salt too large"]) + + # test with max_salt_size=None + del d1.max_salt_size + with self.assertWarningList([]): + self.assertEqual(len(gen_salt(None)), 3) + self.assertEqual(len(gen_salt(5)), 5) + + # TODO: test HasRawSalt mixin def test_30_norm_rounds(self): - "test GenericHandler+HasRounds: .norm_rounds()" + "test GenericHandler + HasRounds mixin" + # setup helpers class d1(uh.HasRounds, uh.GenericHandler): name = 'd1' setting_kwds = ('rounds',) @@ -170,24 +321,45 @@ max_rounds = 3 default_rounds = 2 - #check rounds=None - self.assertEqual(d1.norm_rounds(None), 2) - self.assertRaises(ValueError, d1.norm_rounds, None, strict=True) - - #check small & large rounds - with catch_warnings(): - warnings.filterwarnings("ignore", ".* does not allow (less|more) than \d rounds: .*", UserWarning) - self.assertEqual(d1.norm_rounds(0), 1) - self.assertEqual(d1.norm_rounds(4), 3) - self.assertRaises(ValueError, d1.norm_rounds, 0, strict=True) - self.assertRaises(ValueError, d1.norm_rounds, 4, strict=True) + def norm_rounds(**k): + return d1(**k).rounds + + # check rounds=None + self.assertRaises(TypeError, norm_rounds) + self.assertRaises(TypeError, norm_rounds, rounds=None) + self.assertEqual(norm_rounds(use_defaults=True), 2) + + # check rounds=non int + self.assertRaises(TypeError, norm_rounds, rounds=1.5) + + # check explicit rounds + with catch_warnings(record=True) as wlog: + # too small + self.assertRaises(ValueError, norm_rounds, rounds=0) + self.consumeWarningList(wlog) + + self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1) + self.consumeWarningList(wlog, PasslibHashWarning) + + # just right + self.assertEqual(norm_rounds(rounds=1), 1) + self.assertEqual(norm_rounds(rounds=2), 2) + self.assertEqual(norm_rounds(rounds=3), 3) + self.consumeWarningList(wlog) + + # too large + self.assertRaises(ValueError, norm_rounds, rounds=4) + self.consumeWarningList(wlog) + + self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3) + self.consumeWarningList(wlog, PasslibHashWarning) - #check no default rounds + # check no default rounds d1.default_rounds = None - self.assertRaises(ValueError, d1.norm_rounds, None) + self.assertRaises(TypeError, norm_rounds, use_defaults=True) def test_40_backends(self): - "test GenericHandler+HasManyBackends" + "test GenericHandler + HasManyBackends mixin" class d1(uh.HasManyBackends, uh.GenericHandler): name = 'd1' setting_kwds = () @@ -203,83 +375,196 @@ def _calc_checksum_b(self, secret): return 'b' - #test no backends + # test no backends self.assertRaises(MissingBackendError, d1.get_backend) self.assertRaises(MissingBackendError, d1.set_backend) self.assertRaises(MissingBackendError, d1.set_backend, 'any') self.assertRaises(MissingBackendError, d1.set_backend, 'default') self.assertFalse(d1.has_backend()) - #enable 'b' backend + # enable 'b' backend d1._has_backend_b = True - #test lazy load + # test lazy load obj = d1() - self.assertEqual(obj.calc_checksum('s'), 'b') + self.assertEqual(obj._calc_checksum('s'), 'b') - #test repeat load + # test repeat load d1.set_backend('b') d1.set_backend('any') - self.assertEqual(obj.calc_checksum('s'), 'b') + self.assertEqual(obj._calc_checksum('s'), 'b') - #test unavailable + # test unavailable self.assertRaises(MissingBackendError, d1.set_backend, 'a') self.assertTrue(d1.has_backend('b')) self.assertFalse(d1.has_backend('a')) - #enable 'a' backend also + # enable 'a' backend also d1._has_backend_a = True - #test explicit + # test explicit self.assertTrue(d1.has_backend()) d1.set_backend('a') - self.assertEqual(obj.calc_checksum('s'), 'a') - - #test unknown backend + self.assertEqual(obj._calc_checksum('s'), 'a') + + # test unknown backend self.assertRaises(ValueError, d1.set_backend, 'c') self.assertRaises(ValueError, d1.has_backend, 'c') - def test_50_bh_norm_ident(self): - "test GenericHandler+HasManyIdents: .norm_ident() & .identify()" + def test_50_norm_ident(self): + "test GenericHandler + HasManyIdents" + # setup helpers class d1(uh.HasManyIdents, uh.GenericHandler): name = 'd1' setting_kwds = ('ident',) - ident_values = [ u"!A", u"!B" ] - ident_aliases = { u"A": u"!A"} + default_ident = u("!A") + ident_values = [ u("!A"), u("!B") ] + ident_aliases = { u("A"): u("!A")} + + def norm_ident(**k): + return d1(**k).ident + + # check ident=None + self.assertRaises(TypeError, norm_ident) + self.assertRaises(TypeError, norm_ident, ident=None) + self.assertEqual(norm_ident(use_defaults=True), u('!A')) + + # check valid idents + self.assertEqual(norm_ident(ident=u('!A')), u('!A')) + self.assertEqual(norm_ident(ident=u('!B')), u('!B')) + self.assertRaises(ValueError, norm_ident, ident=u('!C')) + + # check aliases + self.assertEqual(norm_ident(ident=u('A')), u('!A')) + + # check invalid idents + self.assertRaises(ValueError, norm_ident, ident=u('B')) + + # check identify is honoring ident system + self.assertTrue(d1.identify(u("!Axxx"))) + self.assertTrue(d1.identify(u("!Bxxx"))) + self.assertFalse(d1.identify(u("!Cxxx"))) + self.assertFalse(d1.identify(u("A"))) + self.assertFalse(d1.identify(u(""))) + self.assertRaises(TypeError, d1.identify, None) + self.assertRaises(TypeError, d1.identify, 1) + + # check default_ident missing is detected. + d1.default_ident = None + self.assertRaises(AssertionError, norm_ident, use_defaults=True) + + #=================================================================== + # experimental - the following methods are not finished or tested, + # but way work correctly for some hashes + #=================================================================== + def test_91_parsehash(self): + "test parsehash()" + # NOTE: this just tests some existing GenericHandler classes + from passlib import hash + + # + # parsehash() + # + + # simple hash w/ salt + result = hash.des_crypt.parsehash("OgAwTx2l6NADI") + self.assertEqual(result, {'checksum': u('AwTx2l6NADI'), 'salt': u('Og')}) + + # parse rounds and extra implicit_rounds flag + h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9' + s = u('LKO/Ute40T3FNF95') + c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9') + result = hash.sha256_crypt.parsehash(h) + self.assertEqual(result, dict(salt=s, rounds=5000, + implicit_rounds=True, checksum=c)) + + # omit checksum + result = hash.sha256_crypt.parsehash(h, checksum=False) + self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True)) + + # sanitize + result = hash.sha256_crypt.parsehash(h, sanitize=True) + self.assertEqual(result, dict(rounds=5000, implicit_rounds=True, + salt=u('LK**************'), + checksum=u('U0pr***************************************'))) + + # parse w/o implicit rounds flag + result = hash.sha256_crypt.parsehash('$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3') + self.assertEqual(result, dict( + checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'), + salt=u('uy/jIAhCetNCTtb0'), + rounds=10428, + )) + + # parsing of raw checksums & salts + h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k' + result = hash.pbkdf2_sha1.parsehash(h1) + self.assertEqual(result, dict( + checksum=b(';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9'), + rounds=60000, + salt=b('\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ'), + )) + + # sanitizing of raw checksums & salts + result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True) + self.assertEqual(result, dict( + checksum=u('O26************************'), + rounds=60000, + salt=u('Do********************'), + )) + + def test_92_bitsize(self): + "test bitsize()" + # NOTE: this just tests some existing GenericHandler classes + from passlib import hash + + # no rounds + self.assertEqual(hash.des_crypt.bitsize(), + {'checksum': 66, 'salt': 12}) + + # log2 rounds + self.assertEqual(hash.bcrypt.bitsize(), + {'checksum': 186, 'salt': 132}) + + # linear rounds + self.assertEqual(hash.sha256_crypt.bitsize(), + {'checksum': 258, 'rounds': 13, 'salt': 96}) + + # raw checksum + self.assertEqual(hash.pbkdf2_sha1.bitsize(), + {'checksum': 160, 'rounds': 13, 'salt': 128}) + + # TODO: handle fshp correctly, and other glitches noted in code. + ##self.assertEqual(hash.fshp.bitsize(variant=1), + ## {'checksum': 256, 'rounds': 13, 'salt': 128}) + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# PrefixWrapper +#============================================================================= +class dummy_handler_in_registry(object): + "context manager that inserts dummy handler in registry" + def __init__(self, name): + self.name = name + self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict( + name=name, + setting_kwds=(), + )) + + def __enter__(self): + from passlib import registry + registry._unload_handler_name(self.name, locations=False) + registry.register_crypt_handler(self.dummy) + assert registry.get_crypt_handler(self.name) is self.dummy + return self.dummy + + def __exit__(self, *exc_info): + from passlib import registry + registry._unload_handler_name(self.name, locations=False) - #check ident=None w/ no default - self.assertIs(d1.norm_ident(None), None) - self.assertRaises(ValueError, d1.norm_ident, None, strict=True) - - #check ident=None w/ default - d1.default_ident = u"!A" - self.assertEqual(d1.norm_ident(None), u'!A') - self.assertRaises(ValueError, d1.norm_ident, None, strict=True) - - #check explicit - self.assertEqual(d1.norm_ident(u'!A'), u'!A') - self.assertEqual(d1.norm_ident(u'!B'), u'!B') - self.assertRaises(ValueError, d1.norm_ident, u'!C') - - #check aliases - self.assertEqual(d1.norm_ident(u'A'), u'!A') - self.assertRaises(ValueError, d1.norm_ident, u'B') - - #check identify - self.assertTrue(d1.identify(u"!Axxx")) - self.assertTrue(d1.identify(u"!Bxxx")) - self.assertFalse(d1.identify(u"!Cxxx")) - self.assertFalse(d1.identify(u"A")) - self.assertFalse(d1.identify(u"")) - self.assertFalse(d1.identify(None)) - - #========================================================= - #eoc - #========================================================= - -#========================================================= -#PrefixWrapper -#========================================================= class PrefixWrapperTest(TestCase): "test PrefixWrapper class" @@ -287,15 +572,15 @@ "test PrefixWrapper lazy loading of handler" d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}", lazy=True) - #check base state + # check base state self.assertEqual(d1._wrapped_name, "ldap_md5") self.assertIs(d1._wrapped_handler, None) - #check loading works + # check loading works self.assertIs(d1.wrapped, ldap_md5) self.assertIs(d1._wrapped_handler, ldap_md5) - #replace w/ wrong handler, make sure doesn't reload w/ dummy + # replace w/ wrong handler, make sure doesn't reload w/ dummy with dummy_handler_in_registry("ldap_md5") as dummy: self.assertIs(d1.wrapped, ldap_md5) @@ -303,12 +588,12 @@ "test PrefixWrapper active loading of handler" d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}") - #check base state + # check base state self.assertEqual(d1._wrapped_name, "ldap_md5") self.assertIs(d1._wrapped_handler, ldap_md5) self.assertIs(d1.wrapped, ldap_md5) - #replace w/ wrong handler, make sure doesn't reload w/ dummy + # replace w/ wrong handler, make sure doesn't reload w/ dummy with dummy_handler_in_registry("ldap_md5") as dummy: self.assertIs(d1.wrapped, ldap_md5) @@ -317,12 +602,12 @@ d1 = uh.PrefixWrapper("d1", ldap_md5, "{XXX}", "{MD5}") - #check base state + # check base state self.assertEqual(d1._wrapped_name, None) self.assertIs(d1._wrapped_handler, ldap_md5) self.assertIs(d1.wrapped, ldap_md5) - #replace w/ wrong handler, make sure doesn't reload w/ dummy + # replace w/ wrong handler, make sure doesn't reload w/ dummy with dummy_handler_in_registry("ldap_md5") as dummy: self.assertIs(d1.wrapped, ldap_md5) @@ -330,53 +615,120 @@ d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}") self.assertEqual(d1.name, "d1") self.assertIs(d1.setting_kwds, ldap_md5.setting_kwds) + self.assertFalse('max_rounds' in dir(d1)) + + d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}") + self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds) + if SUPPORTS_DIR_METHOD: + self.assertTrue('max_rounds' in dir(d2)) + else: + self.assertFalse('max_rounds' in dir(d2)) def test_11_wrapped_methods(self): d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}") dph = "{XXX}X03MO1qnZdYdgyfeuILPmQ==" lph = "{MD5}X03MO1qnZdYdgyfeuILPmQ==" - #genconfig + # genconfig self.assertIs(d1.genconfig(), None) - #genhash + # genhash self.assertEqual(d1.genhash("password", None), dph) self.assertEqual(d1.genhash("password", dph), dph) self.assertRaises(ValueError, d1.genhash, "password", lph) - #encrypt + # encrypt self.assertEqual(d1.encrypt("password"), dph) - #identify + # identify self.assertTrue(d1.identify(dph)) self.assertFalse(d1.identify(lph)) - #verify + # verify self.assertRaises(ValueError, d1.verify, "password", lph) self.assertTrue(d1.verify("password", dph)) -#========================================================= -#sample algorithms - these serve as known quantities + def test_12_ident(self): + # test ident is proxied + h = uh.PrefixWrapper("h2", "ldap_md5", "{XXX}") + self.assertEqual(h.ident, u("{XXX}{MD5}")) + self.assertIs(h.ident_values, None) + + # test lack of ident means no proxy + h = uh.PrefixWrapper("h2", "des_crypt", "{XXX}") + self.assertIs(h.ident, None) + self.assertIs(h.ident_values, None) + + # test orig_prefix disabled ident proxy + h = uh.PrefixWrapper("h1", "ldap_md5", "{XXX}", "{MD5}") + self.assertIs(h.ident, None) + self.assertIs(h.ident_values, None) + + # test custom ident overrides default + h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{X") + self.assertEqual(h.ident, u("{X")) + self.assertIs(h.ident_values, None) + + # test custom ident must match + h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{XXX}A") + self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5", + "{XXX}", ident="{XY") + self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5", + "{XXX}", ident="{XXXX") + + # test ident_values is proxied + h = uh.PrefixWrapper("h4", "phpass", "{XXX}") + self.assertIs(h.ident, None) + self.assertEqual(h.ident_values, [ u("{XXX}$P$"), u("{XXX}$H$") ]) + + # test ident=True means use prefix even if hash has no ident. + h = uh.PrefixWrapper("h5", "des_crypt", "{XXX}", ident=True) + self.assertEqual(h.ident, u("{XXX}")) + self.assertIs(h.ident_values, None) + + # ... but requires prefix + self.assertRaises(ValueError, uh.PrefixWrapper, "h6", "des_crypt", ident=True) + + # orig_prefix + HasManyIdent - warning + with self.assertWarningList("orig_prefix.*may not work correctly"): + h = uh.PrefixWrapper("h7", "phpass", orig_prefix="$", prefix="?") + self.assertEqual(h.ident_values, None) # TODO: should output (u("?P$"), u("?H$"))) + self.assertEqual(h.ident, None) + + def test_13_repr(self): + "test repr()" + h = uh.PrefixWrapper("h2", "md5_crypt", "{XXX}", orig_prefix="$1$") + self.assertRegex(repr(h), + r"""(?x)^PrefixWrapper\( + ['"]h2['"],\s+ + ['"]md5_crypt['"],\s+ + prefix=u?["']{XXX}['"],\s+ + orig_prefix=u?["']\$1\$['"] + \)$""") + + def test_14_bad_hash(self): + "test orig_prefix sanity check" + # shoudl throw InvalidHashError if wrapped hash doesn't begin + # with orig_prefix. + h = uh.PrefixWrapper("h2", "md5_crypt", orig_prefix="$6$") + self.assertRaises(ValueError, h.encrypt, 'test') + +#============================================================================= +# sample algorithms - these serve as known quantities # to test the unittests themselves, as well as other # parts of passlib. they shouldn't be used as actual password schemes. -#========================================================= +#============================================================================= class UnsaltedHash(uh.StaticHandler): "test algorithm which lacks a salt" name = "unsalted_test_hash" - _stub_config = "0" * 40 - - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, re.compile(u"^[0-9a-f]{40}$")) + checksum_chars = uh.LOWER_HEX_CHARS + checksum_size = 40 - @classmethod - def genhash(cls, secret, hash): - if not cls.identify(hash): - raise ValueError("not a unsalted-example hash") + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") data = b("boblious") + secret - return to_hash_str(hashlib.sha1(data).hexdigest()) + return str_to_uascii(hashlib.sha1(data).hexdigest()) class SaltedHash(uh.HasSalt, uh.GenericHandler): "test algorithm with a salt" @@ -386,58 +738,53 @@ min_salt_size = 2 max_salt_size = 4 checksum_size = 40 - salt_chars = checksum_chars = uh.LC_HEX_CHARS + salt_chars = checksum_chars = uh.LOWER_HEX_CHARS - @classmethod - def identify(cls, hash): - return uh.identify_regexp(hash, re.compile(u"^@salt[0-9a-f]{42,44}$")) + _hash_regex = re.compile(u("^@salt[0-9a-f]{42,44}$")) @classmethod def from_string(cls, hash): if not cls.identify(hash): - raise ValueError("not a salted-example hash") + raise uh.exc.InvalidHashError(cls) if isinstance(hash, bytes): hash = hash.decode("ascii") - return cls(salt=hash[5:-40], checksum=hash[-40:], strict=True) + return cls(salt=hash[5:-40], checksum=hash[-40:]) - _stub_checksum = '0' * 40 + _stub_checksum = u('0') * 40 def to_string(self): - hash = u"@salt%s%s" % (self.salt, self.checksum or self._stub_checksum) - return to_hash_str(hash) + hash = u("@salt%s%s") % (self.salt, self.checksum or self._stub_checksum) + return uascii_to_str(hash) - def calc_checksum(self, secret): + def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") data = self.salt.encode("ascii") + secret + self.salt.encode("ascii") - return to_unicode(hashlib.sha1(data).hexdigest(), "latin-1") + return str_to_uascii(hashlib.sha1(data).hexdigest()) -#========================================================= -#test sample algorithms - really a self-test of HandlerCase -#========================================================= +#============================================================================= +# test sample algorithms - really a self-test of HandlerCase +#============================================================================= -#TODO: provide data samples for algorithms -# (positive knowns, negative knowns, invalid identify) +# TODO: provide data samples for algorithms +# (positive knowns, negative knowns, invalid identify) + +UPASS_TEMP = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2') class UnsaltedHashTest(HandlerCase): handler = UnsaltedHash known_correct_hashes = [ ("password", "61cfd32684c47de231f1f982c214e884133762c0"), + (UPASS_TEMP, '96b329d120b97ff81ada770042e44ba87343ad2b'), ] def test_bad_kwds(self): - if not jython_vm: - #FIXME: annoyingly, the object() constructor of Jython (as of 2.5.2) - # silently drops any extra kwds (old 2.4 behavior) - # instead of raising TypeError (new 2.5 behavior). - # we *could* use a custom base object to restore correct - # behavior, but that's a lot of effort for a non-critical - # border case. so just skipping this test instead... + if not PY_MAX_25: + # annoyingly, py25's ``super().__init__()`` doesn't throw TypeError + # when passing unknown keywords to object. just ignoring + # this issue for now, since it's a minor border case. self.assertRaises(TypeError, UnsaltedHash, salt='x') - self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt=None, strict=True) - self.assertRaises(ValueError, SaltedHash, checksum=SaltedHash._stub_checksum, salt='xxx', strict=True) - self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1) class SaltedHashTest(HandlerCase): @@ -445,10 +792,15 @@ known_correct_hashes = [ ("password", '@salt77d71f8fe74f314dac946766c1ac4a2a58365482c0'), - (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', - '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'), + (UPASS_TEMP, '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'), ] -#========================================================= -#EOF -#========================================================= + def test_bad_kwds(self): + self.assertRaises(TypeError, SaltedHash, + checksum=SaltedHash._stub_checksum, salt=None) + self.assertRaises(ValueError, SaltedHash, + checksum=SaltedHash._stub_checksum, salt='xxx') + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/test_win32.py passlib-1.6.1/passlib/tests/test_win32.py --- passlib-1.5.3/passlib/tests/test_win32.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/tests/test_win32.py 2012-08-01 17:10:03.000000000 +0000 @@ -1,41 +1,51 @@ """tests for passlib.win32 -- (c) Assurance Technologies 2003-2009""" -#========================================================= -#imports -#========================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from binascii import hexlify -#site -#pkg +import warnings +# site +# pkg from passlib.tests.utils import TestCase -#module -import passlib.win32 as mod +# module +from passlib.utils.compat import u -#========================================================= +#============================================================================= # -#========================================================= +#============================================================================= class UtilTest(TestCase): "test util funcs in passlib.win32" ##test hashes from http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx ## among other places + def setUp(self): + super(UtilTest, self).setUp() + warnings.filterwarnings("ignore", + "the 'passlib.win32' module is deprecated") + def test_lmhash(self): + from passlib.win32 import raw_lmhash for secret, hash in [ - ("OLDPASSWORD", u"c9b81d939d6fd80cd408e6b105741864"), - ("NEWPASSWORD", u'09eeab5aa415d6e4d408e6b105741864'), - ("welcome", u"c23413a8a1e7665faad3b435b51404ee"), + ("OLDPASSWORD", u("c9b81d939d6fd80cd408e6b105741864")), + ("NEWPASSWORD", u('09eeab5aa415d6e4d408e6b105741864')), + ("welcome", u("c23413a8a1e7665faad3b435b51404ee")), ]: - result = mod.raw_lmhash(secret, hex=True) + result = raw_lmhash(secret, hex=True) self.assertEqual(result, hash) def test_nthash(self): + warnings.filterwarnings("ignore", + r"nthash\.raw_nthash\(\) is deprecated") + from passlib.win32 import raw_nthash for secret, hash in [ - ("OLDPASSWORD", u"6677b2c394311355b54f25eec5bfacf5"), - ("NEWPASSWORD", u"256781a62031289d3c2c98c14f1efc8c"), + ("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")), + ("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")), ]: - result = mod.raw_nthash(secret, hex=True) + result = raw_nthash(secret, hex=True) self.assertEqual(result, hash) -#========================================================= -#EOF -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/tox_support.py passlib-1.6.1/passlib/tests/tox_support.py --- passlib-1.5.3/passlib/tests/tox_support.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/tests/tox_support.py 2012-05-03 16:36:58.000000000 +0000 @@ -0,0 +1,78 @@ +"""passlib.tests.tox_support - helper script for tox tests""" +#============================================================================= +# init script env +#============================================================================= +import os, sys +root_dir = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) +sys.path.insert(0, root_dir) + +#============================================================================= +# imports +#============================================================================= +# core +import re +import logging; log = logging.getLogger(__name__) +# site +# pkg +from passlib.utils.compat import print_ +# local +__all__ = [ +] + +#============================================================================= +# main +#============================================================================= +TH_PATH = "passlib.tests.test_handlers" + +def do_hash_tests(*args): + "return list of hash algorithm tests that match regexes" + if not args: + print(TH_PATH) + return + suffix = '' + args = list(args) + while True: + if args[0] == "--method": + suffix = '.' + args[1] + del args[:2] + else: + break + from passlib.tests import test_handlers + names = [TH_PATH + ":" + name + suffix for name in dir(test_handlers) + if not name.startswith("_") and any(re.match(arg,name) for arg in args)] + print_("\n".join(names)) + return not names + +def do_preset_tests(name): + "return list of preset test names" + if name == "django" or name == "django-hashes": + do_hash_tests("django_.*_test", "hex_md5_test") + if name == "django": + print_("passlib.tests.test_ext_django") + else: + raise ValueError("unknown name: %r" % name) + +def do_setup_gae(path, runtime): + "write fake GAE ``app.yaml`` to current directory so nosegae will work" + from passlib.tests.utils import set_file + set_file(os.path.join(path, "app.yaml"), """\ +application: fake-app +version: 2 +runtime: %s +api_version: 1 + +handlers: +- url: /.* + script: dummy.py +""" % runtime) + +def main(cmd, *args): + return globals()["do_" + cmd](*args) + +if __name__ == "__main__": + import sys + sys.exit(main(*sys.argv[1:]) or 0) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/tests/utils.py passlib-1.6.1/passlib/tests/utils.py --- passlib-1.5.3/passlib/tests/utils.py 2011-10-08 04:51:13.000000000 +0000 +++ passlib-1.6.1/passlib/tests/utils.py 2012-08-02 14:29:33.000000000 +0000 @@ -1,120 +1,164 @@ """helpers for passlib unittests""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core -import atexit +# core import logging; log = logging.getLogger(__name__) import re import os import sys import tempfile - -try: - import unittest2 as unittest - ut_version = 2 -except ImportError: - import unittest - # Py2k # - if sys.version_info < (2,7): - # Py3k # - #if sys.version_info < (3,2): - # end Py3k # - ut_version = 1 - else: - ut_version = 2 - +import time +from passlib.exc import PasslibHashWarning +from passlib.utils.compat import PY27, PY_MIN_32, PY3, JYTHON import warnings from warnings import warn - -#site -if ut_version < 2: - #used to provide replacement skipTest() method - from nose.plugins.skip import SkipTest -#pkg -from passlib import registry, utils -from passlib.utils import classproperty, handlers as uh, \ - has_rounds_info, has_salt_info, MissingBackendError, \ - rounds_cost_values, b, bytes, native_str, NoneType -#local +# site +# pkg +from passlib.exc import MissingBackendError +import passlib.registry as registry +from passlib.tests.backports import TestCase as _TestCase, catch_warnings, skip, skipIf, skipUnless +from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \ + classproperty, rng, getrandstr, is_ascii_safe, to_native_str, \ + repeat_string, tick +from passlib.utils.compat import b, bytes, iteritems, irange, callable, \ + base_string_types, exc_err, u, unicode, PY2 +import passlib.utils.handlers as uh +# local __all__ = [ - #util funcs - 'enable_option', - 'Params', + # util funcs + 'TEST_MODE', 'set_file', 'get_file', - #unit testing + # unit testing 'TestCase', 'HandlerCase', - 'enable_backend_case', - 'create_backend_case', - - #flags - 'gae_env', ] -#figure out if we're running under GAE... -#some tests (eg FS related) should be skipped. - #XXX: is there better way to do this? +#============================================================================= +# environment detection +#============================================================================= +# figure out if we're running under GAE; +# some tests (e.g. FS writing) should be skipped. +# XXX: is there better way to do this? try: import google.appengine except ImportError: - gae_env = False + GAE = False else: - gae_env = True + GAE = True -#========================================================= -#option flags -#========================================================= -DEFAULT_TESTS = "" - -tests = set( - v.strip() - for v - in os.environ.get("PASSLIB_TESTS", DEFAULT_TESTS).lower().split(",") - ) - -def enable_option(*names): - """check if a given test should be included based on the env var. - - test flags: - all-backends test all backends, even the inactive ones - cover enable minor tweaks to maximize coverage testing - all run all tests +def ensure_mtime_changed(path): + "ensure file's mtime has changed" + # NOTE: this is hack to deal w/ filesystems whose mtime resolution is >= 1s, + # when a test needs to be sure the mtime changed after writing to the file. + last = os.path.getmtime(path) + while os.path.getmtime(path) == last: + time.sleep(0.1) + os.utime(path, None) + +def _get_timer_resolution(timer): + def sample(): + start = cur = timer() + while start == cur: + cur = timer() + return cur-start + return min(sample() for _ in range(3)) +TICK_RESOLUTION = _get_timer_resolution(tick) + +#============================================================================= +# test mode +#============================================================================= +_TEST_MODES = ["quick", "default", "full"] +_test_mode = _TEST_MODES.index(os.environ.get("PASSLIB_TEST_MODE", + "default").strip().lower()) + +def TEST_MODE(min=None, max=None): + """check if test for specified mode should be enabled. + + ``"quick"`` + run the bare minimum tests to ensure functionality. + variable-cost hashes are tested at their lowest setting. + hash algorithms are only tested against the backend that will + be used on the current host. no fuzz testing is done. + + ``"default"`` + same as ``"quick"``, except: hash algorithms are tested + at default levels, and a brief round of fuzz testing is done + for each hash. + + ``"full"`` + extra regression and internal tests are enabled, hash algorithms are tested + against all available backends, unavailable ones are mocked whre possible, + additional time is devoted to fuzz testing. """ - return 'all' in tests or any(name in tests for name in names) + if min and _test_mode < _TEST_MODES.index(min): + return False + if max and _test_mode > _TEST_MODES.index(max): + return False + return True -#========================================================= -#misc utility funcs -#========================================================= -class Params(object): - "helper to represent params for function call" +#============================================================================= +# hash object inspection +#============================================================================= +def has_crypt_support(handler): + "check if host's crypt() supports this natively" + if hasattr(handler, "orig_prefix"): + # ignore wrapper classes + return False + return 'os_crypt' in getattr(handler, "backends", ()) and handler.has_backend("os_crypt") - @classmethod - def norm(cls, value): - if isinstance(value, cls): - return value - if isinstance(value, (list,tuple)): - return cls(*value) - return cls(**value) - - def __init__(self, *args, **kwds): - self.args = args - self.kwds = kwds - - def render(self, offset=0): - """render parenthesized parameters""" - txt = '' - for a in self.args[offset:]: - txt += "%r, " % (a,) - kwds = self.kwds - for k in sorted(kwds): - txt += "%s=%r, " % (k, kwds[k]) - if txt.endswith(", "): - txt = txt[:-2] - return txt +def has_relaxed_setting(handler): + "check if handler supports 'relaxed' kwd" + # FIXME: I've been lazy, should probably just add 'relaxed' kwd + # to all handlers that derive from GenericHandler + + # ignore wrapper classes for now.. though could introspec. + if hasattr(handler, "orig_prefix"): + return False + + return 'relaxed' in handler.setting_kwds or issubclass(handler, + uh.GenericHandler) + +def has_active_backend(handler): + "return active backend for handler, if any" + if not hasattr(handler, "get_backend"): + return "builtin" + try: + return handler.get_backend() + except MissingBackendError: + return None +def is_default_backend(handler, backend): + "check if backend is the default for source" + try: + orig = handler.get_backend() + except MissingBackendError: + return False + try: + return handler.set_backend("default") == backend + finally: + handler.set_backend(orig) + +class temporary_backend(object): + "temporarily set handler to specific backend" + def __init__(self, handler, backend=None): + self.handler = handler + self.backend = backend + + def __enter__(self): + orig = self._orig = self.handler.get_backend() + if self.backend: + self.handler.set_backend(self.backend) + return orig + + def __exit__(self, *exc_info): + self.handler.set_backend(self._orig) + +#============================================================================= +# misc helpers +#============================================================================= def set_file(path, content): "set file to specified bytes" if isinstance(content, unicode): @@ -127,253 +171,312 @@ with open(path, "rb") as fh: return fh.read() -#========================================================= -#custom test base -#========================================================= -class TestCase(unittest.TestCase): +def tonn(source): + "convert native string to non-native string" + if not isinstance(source, str): + return source + elif PY3: + return source.encode("utf-8") + else: + try: + return source.decode("utf-8") + except UnicodeDecodeError: + return source.decode("latin-1") + +def limit(value, lower, upper): + if value < lower: + return lower + elif value > upper: + return upper + return value + +def randintgauss(lower, upper, mu, sigma): + "hack used by fuzz testing" + return int(limit(rng.normalvariate(mu, sigma), lower, upper)) + +def quicksleep(delay): + "because time.sleep() doesn't even have 10ms accuracy on some OSes" + start = tick() + while tick()-start < delay: + pass + +#============================================================================= +# custom test harness +#============================================================================= +class TestCase(_TestCase): """passlib-specific test case class - this class mainly overriddes many of the common assert methods - so to give a default message which includes the values - as well as the class-specific case_prefix string. - this latter bit makes the output of various test cases - easier to distinguish from eachother. + this class adds a number of features to the standard TestCase... + * common prefix for all test descriptions + * resets warnings filter & registry for every test + * tweaks to message formatting + * __msg__ kwd added to assertRaises() + * suite of methods for matching against warnings """ + #=================================================================== + # add various custom features + #=================================================================== + + #--------------------------------------------------------------- + # make it easy for test cases to add common prefix to shortDescription + #--------------------------------------------------------------- - #============================================================= - #make it ease for test cases to add common prefix to all descs - #============================================================= - #: string or method returning string - prepended to all tests in TestCase - case_prefix = None - - #: flag to disable feature - longDescription = True + # string prepended to all tests in TestCase + descriptionPrefix = None def shortDescription(self): - "wrap shortDescription() method to prepend case_prefix" + "wrap shortDescription() method to prepend descriptionPrefix" desc = super(TestCase, self).shortDescription() - if desc is None: - #would still like to add prefix, but munges things up. - return None - prefix = self.case_prefix - if prefix and self.longDescription: - if callable(prefix): - prefix = prefix() - desc = "%s: %s" % (prefix, desc) + prefix = self.descriptionPrefix + if prefix: + desc = "%s: %s" % (prefix, desc or str(self)) return desc - #============================================================ - #hack to set UT2 private skip attrs to mirror nose's __test__ attr - #============================================================ - if ut_version >= 2: - - @classproperty - def __unittest_skip__(cls): - return not getattr(cls, "__test__", True) + #--------------------------------------------------------------- + # hack things so nose and ut2 both skip subclasses who have + # "__unittest_skip=True" set, or whose names start with "_" + #--------------------------------------------------------------- + @classproperty + def __unittest_skip__(cls): + # NOTE: this attr is technically a unittest2 internal detail. + name = cls.__name__ + return name.startswith("_") or \ + getattr(cls, "_%s__unittest_skip" % name, False) + + # make this mirror nose's '__test__' attr + return not getattr(cls, "__test__", True) @classproperty def __test__(cls): - #so nose won't auto run *this* cls, but it will for subclasses - return cls is not TestCase and not cls.__name__.startswith("_") + # make nose just proxy __unittest_skip__ + return not cls.__unittest_skip__ - #============================================================ - # tweak msg formatting for some assert methods - #============================================================ - longMessage = True #override python default (False) + # flag to skip *this* class + __unittest_skip = True - def _formatMessage(self, msg, std): - "override UT2's _formatMessage - only use longMessage if msg ends with ':'" - if not msg: - return std - if not self.longMessage or not msg.endswith(":"): - return msg.rstrip(":") - return '%s %s' % (msg, std) - - #============================================================ - #override some unittest1 methods to support _formatMessage - #============================================================ - if ut_version < 2: - - def assertEqual(self, real, correct, msg=None): - if real != correct: - std = "got %r, expected would equal %r" % (real, correct) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - - def assertNotEqual(self, real, correct, msg=None): - if real == correct: - std = "got %r, expected would not equal %r" % (real, correct) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - - assertEquals = assertEqual - assertNotEquals = assertNotEqual - - #NOTE: overriding this even under UT2. - #FIXME: this doesn't support the fancy context manager UT2 provides. - def assertRaises(self, type, func, *args, **kwds): - #NOTE: overriding this for format ability, - # but ALSO adding "__msg__" kwd so we can set custom msg - msg = kwds.pop("__msg__", None) - try: - result = func(*args, **kwds) - except Exception, err: - if isinstance(err, type): - return True - ##import traceback, sys - ##print >>sys.stderr, traceback.print_exception(*sys.exc_info()) - std = "function raised %r, expected %r" % (err, type) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - std = "function returned %r, expected it to raise %r" % (result, type) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - - #=============================================================== - #backport some methods from unittest2 - #=============================================================== - if ut_version < 2: - - def assertIs(self, real, correct, msg=None): - if real is not correct: - std = "got %r, expected would be %r" % (real, correct) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - - def assertIsNot(self, real, correct, msg=None): - if real is correct: - std = "got %r, expected would not be %r" % (real, correct) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - - def assertIsInstance(self, obj, klass, msg=None): - if not isinstance(obj, klass): - std = "got %r, expected instance of %r" % (obj, klass) - msg = self._formatMessage(msg, std) - raise self.failureException(msg) - - def skipTest(self, reason): - raise SkipTest(reason) - - def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None): - """Fail if the two objects are unequal as determined by their - difference rounded to the given number of decimal places - (default 7) and comparing to zero, or by comparing that the - between the two objects is more than the given delta. - - Note that decimal places (from zero) are usually not the same - as significant digits (measured from the most signficant digit). - - If the two objects compare equal then they will automatically - compare almost equal. - """ - if first == second: - # shortcut - return - if delta is not None and places is not None: - raise TypeError("specify delta or places not both") + #--------------------------------------------------------------- + # reset warning filters & registry before each test + #--------------------------------------------------------------- - if delta is not None: - if abs(first - second) <= delta: - return + # flag to reset all warning filters & ignore state + resetWarningState = True - standardMsg = '%s != %s within %s delta' % (repr(first), - repr(second), - repr(delta)) - else: - if places is None: - places = 7 + def setUp(self): + super(TestCase, self).setUp() + self.setUpWarnings() - if round(abs(second-first), places) == 0: - return + def setUpWarnings(self): + "helper to init warning filters before subclass setUp()" + if self.resetWarningState: + ctx = reset_warnings() + ctx.__enter__() + self.addCleanup(ctx.__exit__) - standardMsg = '%s != %s within %r places' % (repr(first), - repr(second), - places) - msg = self._formatMessage(msg, standardMsg) - raise self.failureException(msg) + #--------------------------------------------------------------- + # tweak message formatting so longMessage mode is only enabled + # if msg ends with ":", and turn on longMessage by default. + #--------------------------------------------------------------- + longMessage = True - if not hasattr(unittest.TestCase, "assertRegexpMatches"): - #added in 2.7/UT2 and 3.1 - def assertRegexpMatches(self, text, expected_regex, msg=None): - """Fail the test unless the text matches the regular expression.""" - if isinstance(expected_regex, basestring): - assert expected_regex, "expected_regex must not be empty." - expected_regex = re.compile(expected_regex) - if not expected_regex.search(text): - msg = msg or "Regex didn't match" - msg = '%s: %r not found in %r' % (msg, expected_regex.pattern, text) - raise self.failureException(msg) - - #============================================================ - #add some custom methods - #============================================================ - def assertFunctionResults(self, func, cases): - """helper for running through function calls. - - func should be the function to call. - cases should be list of Param instances, - where first position argument is expected return value, - and remaining args and kwds are passed to function. - """ - for elem in cases: - elem = Params.norm(elem) - correct = elem.args[0] - result = func(*elem.args[1:], **elem.kwds) - msg = "error for case %r:" % (elem.render(1),) - self.assertEqual(result, correct, msg) + def _formatMessage(self, msg, std): + if self.longMessage and msg and msg.rstrip().endswith(":"): + return '%s %s' % (msg.rstrip(), std) + else: + return msg or std - def assertWarningMatches(self, warning, - message=None, message_re=None, + #--------------------------------------------------------------- + # override assertRaises() to support '__msg__' keyword + #--------------------------------------------------------------- + def assertRaises(self, _exc_type, _callable=None, *args, **kwds): + msg = kwds.pop("__msg__", None) + if _callable is None: + # FIXME: this ignores 'msg' + return super(TestCase, self).assertRaises(_exc_type, None, + *args, **kwds) + try: + result = _callable(*args, **kwds) + except _exc_type: + return + std = "function returned %r, expected it to raise %r" % (result, + _exc_type) + raise self.failureException(self._formatMessage(msg, std)) + + #--------------------------------------------------------------- + # forbid a bunch of deprecated aliases so I stop using them + #--------------------------------------------------------------- + def assertEquals(self, *a, **k): + raise AssertionError("this alias is deprecated by unittest2") + assertNotEquals = assertRegexMatches = assertEquals + + #=================================================================== + # custom methods for matching warnings + #=================================================================== + def assertWarning(self, warning, + message_re=None, message=None, category=None, - ##filename=None, filename_re=None, - ##lineno=None, + filename_re=None, filename=None, + lineno=None, msg=None, ): - "check if WarningMessage instance (as returned by catch_warnings) matches parameters" - - #determine if we have WarningMessage object, - #and ensure 'warning' contains only warning instances. + """check if warning matches specified parameters. + 'warning' is the instance of Warning to match against; + can also be instance of WarningMessage (as returned by catch_warnings). + """ + # check input type if hasattr(warning, "category"): + # resolve WarningMessage -> Warning, but preserve original wmsg = warning warning = warning.message else: + # no original WarningMessage, passed raw Warning wmsg = None - #tests that can use a warning instance or WarningMessage object + # tests that can use a warning instance or WarningMessage object if message: self.assertEqual(str(warning), message, msg) if message_re: - self.assertRegexpMatches(str(warning), message_re, msg) + self.assertRegex(str(warning), message_re, msg) if category: self.assertIsInstance(warning, category, msg) - #commented out until needed... - ###tests that require a WarningMessage object - ##if filename or filename_re: - ## if not wmsg: - ## raise TypeError("can't read filename from warning object") - ## real = wmsg.filename - ## if real.endswith(".pyc") or real.endswith(".pyo"): - ## #FIXME: should use a stdlib call to resolve this back - ## # to original module's path - ## real = real[:-1] - ## if filename: - ## self.assertEqual(real, filename, msg) - ## if filename_re: - ## self.assertRegexpMatches(real, filename_re, msg) - ##if lineno: - ## if not wmsg: - ## raise TypeError("can't read lineno from warning object") - ## self.assertEqual(wmsg.lineno, lineno, msg) - - #============================================================ - #eoc - #============================================================ - -#========================================================= -#other unittest helpers -#========================================================= + # tests that require a WarningMessage object + if filename or filename_re: + if not wmsg: + raise TypeError("matching on filename requires a " + "WarningMessage instance") + real = wmsg.filename + if real.endswith(".pyc") or real.endswith(".pyo"): + # FIXME: should use a stdlib call to resolve this back + # to module's original filename. + real = real[:-1] + if filename: + self.assertEqual(real, filename, msg) + if filename_re: + self.assertRegex(real, filename_re, msg) + if lineno: + if not wmsg: + raise TypeError("matching on lineno requires a " + "WarningMessage instance") + self.assertEqual(wmsg.lineno, lineno, msg) + + class _AssertWarningList(catch_warnings): + """context manager for assertWarningList()""" + def __init__(self, case, **kwds): + self.case = case + self.kwds = kwds + self.__super = super(TestCase._AssertWarningList, self) + self.__super.__init__(record=True) + + def __enter__(self): + self.log = self.__super.__enter__() + + def __exit__(self, *exc_info): + self.__super.__exit__(*exc_info) + if not exc_info: + self.case.assertWarningList(self.log, **self.kwds) + + def assertWarningList(self, wlist=None, desc=None, msg=None): + """check that warning list (e.g. from catch_warnings) matches pattern""" + if desc is None: + assert wlist is not None + return self._AssertWarningList(self, desc=wlist, msg=msg) + # TODO: make this display better diff of *which* warnings did not match + assert desc is not None + if not isinstance(desc, (list,tuple)): + desc = [desc] + for idx, entry in enumerate(desc): + if isinstance(entry, str): + entry = dict(message_re=entry) + elif isinstance(entry, type) and issubclass(entry, Warning): + entry = dict(category=entry) + elif not isinstance(entry, dict): + raise TypeError("entry must be str, warning, or dict") + try: + data = wlist[idx] + except IndexError: + break + self.assertWarning(data, msg=msg, **entry) + else: + if len(wlist) == len(desc): + return + std = "expected %d warnings, found %d: wlist=%s desc=%r" % \ + (len(desc), len(wlist), self._formatWarningList(wlist), desc) + raise self.failureException(self._formatMessage(msg, std)) + + def consumeWarningList(self, wlist, desc=None, *args, **kwds): + """[deprecated] assertWarningList() variant that clears list afterwards""" + if desc is None: + desc = [] + self.assertWarningList(wlist, desc, *args, **kwds) + del wlist[:] + + def _formatWarning(self, entry): + tail = "" + if hasattr(entry, "message"): + # WarningMessage instance. + tail = " filename=%r lineno=%r" % (entry.filename, entry.lineno) + if entry.line: + tail += " line=%r" % (entry.line,) + entry = entry.message + cls = type(entry) + return "<%s.%s message=%r%s>" % (cls.__module__, cls.__name__, + str(entry), tail) + + def _formatWarningList(self, wlist): + return "[%s]" % ", ".join(self._formatWarning(entry) for entry in wlist) + + #=================================================================== + # capability tests + #=================================================================== + def require_stringprep(self): + "helper to skip test if stringprep is missing" + from passlib.utils import stringprep + if not stringprep: + from passlib.utils import _stringprep_missing_reason + raise self.skipTest("not available - stringprep module is " + + _stringprep_missing_reason) + + def require_TEST_MODE(self, level): + "skip test for all PASSLIB_TEST_MODE values below " + if not TEST_MODE(level): + raise self.skipTest("requires >= %r test mode" % level) + + def require_writeable_filesystem(self): + "skip test if writeable FS not available" + if GAE: + return self.skipTest("GAE doesn't offer read/write filesystem access") + + #=================================================================== + # other + #=================================================================== + _mktemp_queue = None + + def mktemp(self, *args, **kwds): + "create temp file that's cleaned up at end of test" + self.require_writeable_filesystem() + fd, path = tempfile.mkstemp(*args, **kwds) + os.close(fd) + queue = self._mktemp_queue + if queue is None: + queue = self._mktemp_queue = [] + def cleaner(): + for path in queue: + if os.path.exists(path): + os.remove(path) + del queue[:] + self.addCleanup(cleaner) + queue.append(path) + return path + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# other unittest helpers +#============================================================================= +RESERVED_BACKEND_NAMES = ["any", "default"] + class HandlerCase(TestCase): """base class for testing password hash handlers (esp passlib.utils.handlers subclasses) @@ -391,61 +494,200 @@ This is subclass of :class:`unittest.TestCase` (or :class:`unittest2.TestCase` if available). """ - #========================================================= - #attrs to be filled in by subclass for testing specific handler - #========================================================= + #=================================================================== + # class attrs - should be filled in by subclass + #=================================================================== + + #--------------------------------------------------------------- + # handler setup + #--------------------------------------------------------------- - #: specify handler object here (required) + # handler class to test [required] handler = None - #: maximum number of chars which hash will include in checksum - # override this only if hash doesn't use all chars (the default) - secret_chars = -1 + # if set, run tests against specified backend + backend = None - #: list of (secret,hash) pairs which handler should verify as matching + #--------------------------------------------------------------- + # test vectors + #--------------------------------------------------------------- + + # list of (secret, hash) tuples which are known to be correct known_correct_hashes = [] - #: list of (config, secret, hash) triples which handler should genhash & verify + # list of (config, secret, hash) tuples are known to be correct known_correct_configs = [] - #: hashes so malformed they aren't even identified properly + # list of (alt_hash, secret, hash) tuples, where alt_hash is a hash + # using an alternate representation that should be recognized and verify + # correctly, but should be corrected to match hash when passed through + # genhash() + known_alternate_hashes = [] + + # hashes so malformed they aren't even identified properly known_unidentified_hashes = [] - #: hashes which are malformed - they should identify() as True, but cause error when passed to genhash/verify + # hashes which are identifiabled but malformed - they should identify() + # as True, but cause an error when passed to genhash/verify. known_malformed_hashes = [] - #: list of (handler name, hash) pairs for other algorithm's hashes, that handler shouldn't identify as belonging to it - # this list should generally be sufficient (if handler name in list, that entry will be skipped) + # list of (handler name, hash) pairs for other algorithm's hashes that + # handler shouldn't identify as belonging to it this list should generally + # be sufficient (if handler name in list, that entry will be skipped) known_other_hashes = [ ('des_crypt', '6f8c114b58f2c'), ('md5_crypt', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), - ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwc" - "elCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"), + ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywW" + "vt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"), ] - #: flag if scheme accepts empty string as hash (rare) - accepts_empty_hash = False + # passwords used to test basic encrypt behavior - generally + # don't need to be overidden. + stock_passwords = [ + u("test"), + u("\u20AC\u00A5$"), + b('\xe2\x82\xac\xc2\xa5$') + ] - #: if handler uses multiple backends, explicitly set this one when running tests. - backend = None + #--------------------------------------------------------------- + # option flags + #--------------------------------------------------------------- + + # maximum number of chars which hash will include in digest. + # ``None`` (the default) indicates the hash uses ALL of the password. + secret_size = None + + # whether hash is case insensitive + # True, False, or special value "verify-only" (which indicates + # hash contains case-sensitive portion, but verifies is case-insensitive) + secret_case_insensitive = False + + # flag if scheme accepts ALL hash strings (e.g. plaintext) + accepts_all_hashes = False + + # flag indicating "disabled account" handler (e.g. unix_disabled) + is_disabled_handler = False + + # flag/hack to filter PasslibHashWarning issued by test_72_configs() + filter_config_warnings = False + + # forbid certain characters in passwords + @classproperty + def forbidden_characters(cls): + # anything that supports crypt() interface should forbid null chars, + # since crypt() uses null-terminated strings. + if 'os_crypt' in getattr(cls.handler, "backends", ()): + return b("\x00") + return None - #: hack used by create_backend() to signal we should monkeypatch - # safe_os_crypt() to use handler+this backend, - # only used when backend == "os_crypt" - _patch_crypt_backend = None - - #========================================================= - #alg interface helpers - allows subclass to overide how - # default tests invoke the handler (eg for context_kwds) - #========================================================= + #=================================================================== + # internal class attrs + #=================================================================== + __unittest_skip = True + + @property + def descriptionPrefix(self): + handler = self.handler + name = handler.name + if hasattr(handler, "get_backend"): + name += " (%s backend)" % (handler.get_backend(),) + return name + + #=================================================================== + # internal instance attrs + #=================================================================== + # indicates safe_crypt() has been patched to use another backend of handler. + using_patched_crypt = False + + #=================================================================== + # support methods + #=================================================================== + + #--------------------------------------------------------------- + # configuration helpers + #--------------------------------------------------------------- + @property + def supports_config_string(self): + return self.do_genconfig() is not None + + @classmethod + def iter_known_hashes(cls): + "iterate through known (secret, hash) pairs" + for secret, hash in cls.known_correct_hashes: + yield secret, hash + for config, secret, hash in cls.known_correct_configs: + yield secret, hash + for alt, secret, hash in cls.known_alternate_hashes: + yield secret, hash + + def get_sample_hash(self): + "test random sample secret/hash pair" + known = list(self.iter_known_hashes()) + return rng.choice(known) + + #--------------------------------------------------------------- + # test helpers + #--------------------------------------------------------------- + def check_verify(self, secret, hash, msg=None, negate=False): + "helper to check verify() outcome, honoring is_disabled_handler" + result = self.do_verify(secret, hash) + self.assertTrue(result is True or result is False, + "verify() returned non-boolean value: %r" % (result,)) + if self.is_disabled_handler or negate: + if not result: + return + if not msg: + msg = ("verify incorrectly returned True: secret=%r, hash=%r" % + (secret, hash)) + raise self.failureException(msg) + else: + if result: + return + if not msg: + msg = "verify failed: secret=%r, hash=%r" % (secret, hash) + raise self.failureException(msg) + + def check_returned_native_str(self, result, func_name): + self.assertIsInstance(result, str, + "%s() failed to return native string: %r" % (func_name, result,)) + + #--------------------------------------------------------------- + # PasswordHash helpers - wraps all calls to PasswordHash api, + # so that subclasses can fill in defaults and account for other specialized behavior + #--------------------------------------------------------------- + def populate_settings(self, kwds): + "subclassable method to populate default settings" + # use lower rounds settings for certain test modes + handler = self.handler + if 'rounds' in handler.setting_kwds and 'rounds' not in kwds: + mn = handler.min_rounds + df = handler.default_rounds + if TEST_MODE(max="quick"): + # use minimum rounds for quick mode + kwds['rounds'] = max(3, mn) + else: + # use default/16 otherwise + factor = 3 + if getattr(handler, "rounds_cost", None) == "log2": + df -= factor + else: + df = df//(1<= 1") - #check min_salt_size + # check min_salt_size if cls.min_salt_size < 0: raise AssertionError("min_salt_chars must be >= 0") if mx_set and cls.min_salt_size > cls.max_salt_size: raise AssertionError("min_salt_chars must be <= max_salt_chars") - #check default_salt_size + # check default_salt_size if cls.default_salt_size < cls.min_salt_size: raise AssertionError("default_salt_size must be >= min_salt_size") if mx_set and cls.default_salt_size > cls.max_salt_size: raise AssertionError("default_salt_size must be <= max_salt_size") - #check for 'salt_size' keyword + # check for 'salt_size' keyword if 'salt_size' not in cls.setting_kwds and \ (not mx_set or cls.min_salt_size < cls.max_salt_size): - #NOTE: for now, only bothering to issue warning if default_salt_size isn't maxed out + # NOTE: only bothering to issue warning if default_salt_size + # isn't maxed out if (not mx_set or cls.default_salt_size < cls.max_salt_size): - warn("%s: hash handler supports range of salt sizes, but doesn't offer 'salt_size' setting" % (cls.name,)) + warn("%s: hash handler supports range of salt sizes, " + "but doesn't offer 'salt_size' setting" % (cls.name,)) - #check salt_chars & default_salt_chars + # check salt_chars & default_salt_chars if cls.salt_chars: if not cls.default_salt_chars: raise AssertionError("default_salt_chars must not be empty") @@ -607,627 +1024,1206 @@ if not cls.default_salt_chars: raise AssertionError("default_salt_chars MUST be specified if salt_chars is empty") - def test_02_optional_rounds_attributes(self): - "validate optional rounds attributes" - cls = self.handler - if not has_rounds_info(cls): + @property + def salt_bits(self): + "calculate number of salt bits in hash" + # XXX: replace this with bitsize() method? + handler = self.handler + assert has_salt_info(handler), "need explicit bit-size for " + handler.name + from math import log + # FIXME: this may be off for case-insensitive hashes, but that accounts + # for ~1 bit difference, which is good enough for test_11() + return int(handler.default_salt_size * + log(len(handler.default_salt_chars), 2)) + + def test_11_unique_salt(self): + "test encrypt() / genconfig() creates new salt each time" + self.require_salt() + # odds of picking 'n' identical salts at random is '(.5**salt_bits)**n'. + # we want to pick the smallest N needed s.t. odds are <1/1000, just + # to eliminate false-positives. which works out to n>7-salt_bits. + # n=1 is sufficient for most hashes, but a few border cases (e.g. + # cisco_type7) have < 7 bits of salt, requiring more. + samples = max(1,7-self.salt_bits) + def sampler(func): + value1 = func() + for i in irange(samples): + value2 = func() + if value1 != value2: + return + raise self.failureException("failed to find different salt after " + "%d samples" % (samples,)) + if self.do_genconfig() is not None: # cisco_type7 has salt & no config + sampler(self.do_genconfig) + sampler(lambda : self.do_encrypt("stub")) + + def test_12_min_salt_size(self): + "test encrypt() / genconfig() honors min_salt_size" + self.require_salt_info() + + handler = self.handler + salt_char = handler.salt_chars[0:1] + min_size = handler.min_salt_size + + # + # check min is accepted + # + s1 = salt_char * min_size + self.do_genconfig(salt=s1) + + self.do_encrypt('stub', salt_size=min_size) + + # + # check min-1 is rejected + # + if min_size > 0: + self.assertRaises(ValueError, self.do_genconfig, + salt=s1[:-1]) + + self.assertRaises(ValueError, self.do_encrypt, 'stub', + salt_size=min_size-1) + + def test_13_max_salt_size(self): + "test encrypt() / genconfig() honors max_salt_size" + self.require_salt_info() + + handler = self.handler + max_size = handler.max_salt_size + salt_char = handler.salt_chars[0:1] + + if max_size is None: + # + # if it's not set, salt should never be truncated; so test it + # with an unreasonably large salt. + # + s1 = salt_char * 1024 + c1 = self.do_genconfig(salt=s1) + c2 = self.do_genconfig(salt=s1 + salt_char) + self.assertNotEqual(c1, c2) + + self.do_encrypt('stub', salt_size=1024) + + else: + # + # check max size is accepted + # + s1 = salt_char * max_size + c1 = self.do_genconfig(salt=s1) + + self.do_encrypt('stub', salt_size=max_size) + + # + # check max size + 1 is rejected + # + s2 = s1 + salt_char + self.assertRaises(ValueError, self.do_genconfig, salt=s2) + + self.assertRaises(ValueError, self.do_encrypt, 'stub', + salt_size=max_size+1) + + # + # should accept too-large salt in relaxed mode + # + if has_relaxed_setting(handler): + with catch_warnings(record=True): # issues passlibhandlerwarning + c2 = self.do_genconfig(salt=s2, relaxed=True) + self.assertEqual(c2, c1) + + # + # if min_salt supports it, check smaller than mx is NOT truncated + # + if handler.min_salt_size < max_size: + c3 = self.do_genconfig(salt=s1[:-1]) + self.assertNotEqual(c3, c1) + + def prepare_salt(self, salt): + "prepare generated salt" + if self.handler.name in ["bcrypt", "django_bcrypt"]: + from passlib.utils import bcrypt64 + salt = bcrypt64.repair_unused(salt) + return salt + + def test_14_salt_chars(self): + "test genconfig() honors salt_chars" + self.require_salt_info() + + handler = self.handler + mx = handler.max_salt_size + mn = handler.min_salt_size + cs = handler.salt_chars + raw = isinstance(cs, bytes) + + # make sure all listed chars are accepted + chunk = mx or 32 + for i in irange(0,len(cs),chunk): + salt = cs[i:i+chunk] + if len(salt) < mn: + salt = (salt*(mn//len(salt)+1))[:chunk] + salt = self.prepare_salt(salt) + self.do_genconfig(salt=salt) + + # check some invalid salt chars, make sure they're rejected + source = u('\x00\xff') + if raw: + source = source.encode("latin-1") + chunk = max(mn, 1) + for c in source: + if c not in cs: + self.assertRaises(ValueError, self.do_genconfig, salt=c*chunk, + __msg__="invalid salt char %r:" % (c,)) + + @property + def salt_type(self): + "hack to determine salt keyword's datatype" + # NOTE: cisco_type7 uses 'int' + if getattr(self.handler, "_salt_is_bytes", False): + return bytes + else: + return unicode + + def test_15_salt_type(self): + "test non-string salt values" + self.require_salt() + salt_type = self.salt_type + + # should always throw error for random class. + class fake(object): + pass + self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=fake()) + + # unicode should be accepted only if salt_type is unicode. + if salt_type is not unicode: + self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=u('x')) + + # bytes should be accepted only if salt_type is bytes, + # OR if salt type is unicode and running PY2 - to allow native strings. + if not (salt_type is bytes or (PY2 and salt_type is unicode)): + self.assertRaises(TypeError, self.do_encrypt, 'stub', salt=b('x')) + + #=================================================================== + # rounds + #=================================================================== + def require_rounds_info(self): + if not has_rounds_info(self.handler): raise self.skipTest("handler lacks rounds attributes") + def test_20_optional_rounds_attributes(self): + "validate optional rounds attributes" + self.require_rounds_info() + + cls = self.handler AssertionError = self.failureException - #check max_rounds + # check max_rounds if cls.max_rounds is None: raise AssertionError("max_rounds not specified") if cls.max_rounds < 1: raise AssertionError("max_rounds must be >= 1") - #check min_rounds + # check min_rounds if cls.min_rounds < 0: raise AssertionError("min_rounds must be >= 0") if cls.min_rounds > cls.max_rounds: raise AssertionError("min_rounds must be <= max_rounds") - #check default_rounds + # check default_rounds if cls.default_rounds is not None: if cls.default_rounds < cls.min_rounds: raise AssertionError("default_rounds must be >= min_rounds") if cls.default_rounds > cls.max_rounds: raise AssertionError("default_rounds must be <= max_rounds") - #check rounds_cost + # check rounds_cost if cls.rounds_cost not in rounds_cost_values: raise AssertionError("unknown rounds cost constant: %r" % (cls.rounds_cost,)) - def test_03_HasManyIdents(self): - "check configuration of HasManyIdents-derived classes" + def test_21_rounds_limits(self): + "test encrypt() / genconfig() honors rounds limits" + self.require_rounds_info() + handler = self.handler + min_rounds = handler.min_rounds + + # check min is accepted + self.do_genconfig(rounds=min_rounds) + self.do_encrypt('stub', rounds=min_rounds) + + # check min-1 is rejected + self.assertRaises(ValueError, self.do_genconfig, rounds=min_rounds-1) + self.assertRaises(ValueError, self.do_encrypt, 'stub', + rounds=min_rounds-1) + + # TODO: check relaxed mode clips min-1 + + # handle max rounds + max_rounds = handler.max_rounds + if max_rounds is None: + # check large value is accepted + self.do_genconfig(rounds=(1<<31)-1) + else: + # check max is accepted + self.do_genconfig(rounds=max_rounds) + + # check max+1 is rejected + self.assertRaises(ValueError, self.do_genconfig, + rounds=max_rounds+1) + self.assertRaises(ValueError, self.do_encrypt, 'stub', + rounds=max_rounds+1) + + # TODO: check relaxed mode clips max+1 + + #=================================================================== + # idents + #=================================================================== + def test_30_HasManyIdents(self): + "validate HasManyIdents configuration" cls = self.handler if not isinstance(cls, type) or not issubclass(cls, uh.HasManyIdents): raise self.skipTest("handler doesn't derive from HasManyIdents") - #check settings + # check settings self.assertTrue('ident' in cls.setting_kwds) - #check ident_values list + # check ident_values list for value in cls.ident_values: self.assertIsInstance(value, unicode, "cls.ident_values must be unicode:") self.assertTrue(len(cls.ident_values)>1, "cls.ident_values must have 2+ elements:") - #check default_ident value + # check default_ident value self.assertIsInstance(cls.default_ident, unicode, "cls.default_ident must be unicode:") self.assertTrue(cls.default_ident in cls.ident_values, "cls.default_ident must specify member of cls.ident_values") - #check optional aliases list + # check optional aliases list if cls.ident_aliases: - for alias, ident in cls.ident_aliases.iteritems(): + for alias, ident in iteritems(cls.ident_aliases): self.assertIsInstance(alias, unicode, - "cls.ident_aliases keys must be unicode:") #XXX: allow ints? + "cls.ident_aliases keys must be unicode:") # XXX: allow ints? self.assertIsInstance(ident, unicode, "cls.ident_aliases values must be unicode:") self.assertTrue(ident in cls.ident_values, "cls.ident_aliases must map to cls.ident_values members: %r" % (ident,)) - RESERVED_BACKEND_NAMES = [ "any", "default", None ] + # check constructor validates ident correctly. + handler = cls + hash = self.get_sample_hash()[1] + kwds = handler.parsehash(hash) + del kwds['ident'] + + # ... accepts good ident + handler(ident=cls.default_ident, **kwds) + + # ... requires ident w/o defaults + self.assertRaises(TypeError, handler, **kwds) + + # ... supplies default ident + handler(use_defaults=True, **kwds) + + # ... rejects bad ident + self.assertRaises(ValueError, handler, ident='xXx', **kwds) + + # TODO: check various supported idents + + #=================================================================== + # passwords + #=================================================================== + def test_60_secret_size(self): + "test password size limits" + sc = self.secret_size + base = "too many secrets" # 16 chars + alt = 'x' # char that's not in base string + if sc is not None: + # hash only counts the first characters; eg: bcrypt, des-crypt - def test_04_backend_handler(self): - "check behavior of multiple-backend handlers" - h = self.handler - if not hasattr(h, "set_backend"): - raise self.skipTest("handler has single backend") + # create & hash string that's exactly sc+1 chars + secret = repeat_string(base, sc+1) + hash = self.do_encrypt(secret) - #preserve current backend - orig = h.get_backend() - try: - #run through all backends handler supports - for backend in h.backends: - self.assertFalse(backend in self.RESERVED_BACKEND_NAMES, - "invalid backend name: %r" % (backend,)) - #check has_backend() returns bool value - r = h.has_backend(backend) - if r is True: - #check backend can be loaded - h.set_backend(backend) - self.assertEqual(h.get_backend(), backend) - elif r is False: - #check backend CAN'T be loaded - self.assertRaises(MissingBackendError, h.set_backend, backend) - else: - #failure eg: used classmethod instead of classproperty in _has_backend_xxx - raise TypeError("has_backend(%r) returned invalid value: %r" % (backend, r,)) - finally: - h.set_backend(orig) - - #========================================================= - #identify() - #========================================================= - def test_10_identify_hash(self): - "test identify() against scheme's own hashes" - for secret, hash in self.known_correct_hashes: - self.assertEqual(self.do_identify(hash), True, "hash=%r:" % (hash,)) + # check sc value isn't too large by verifying that sc-1'th char + # affects hash + secret2 = secret[:-2] + alt + secret[-1] + self.assertFalse(self.do_verify(secret2, hash), + "secret_size value is too large") + + # check sc value isn't too small by verifying adding sc'th char + # *doesn't* affect hash + secret3 = secret[:-1] + alt + self.assertTrue(self.do_verify(secret3, hash), + "secret_size value is too small") - for config, secret, hash in self.known_correct_configs: - self.assertEqual(self.do_identify(hash), True, "hash=%r:" % (hash,)) + else: + # hash counts all characters; e.g. md5-crypt - def test_11_identify_config(self): - "test identify() against scheme's own config strings" - if not self.known_correct_configs: - raise self.skipTest("no config strings provided") - for config, secret, hash in self.known_correct_configs: - self.assertEqual(self.do_identify(config), True, "config=%r:" % (config,)) + # NOTE: this doesn't do an exhaustive search to verify algorithm + # doesn't have some cutoff point, it just tries + # 1024-character string, and alters the last char. + # as long as algorithm doesn't clip secret at point <1024, + # the new secret shouldn't verify. + secret = base * 64 + hash = self.do_encrypt(secret) + secret2 = secret[:-1] + alt + self.assertFalse(self.do_verify(secret2, hash), + "full password not used in digest") + + def test_61_secret_case_sensitive(self): + "test password case sensitivity" + hash_insensitive = self.secret_case_insensitive is True + verify_insensitive = self.secret_case_insensitive in [True, + "verify-only"] + + lower = 'test' + upper = 'TEST' + h1 = self.do_encrypt(lower) + if verify_insensitive and not self.is_disabled_handler: + self.assertTrue(self.do_verify(upper, h1), + "verify() should not be case sensitive") + else: + self.assertFalse(self.do_verify(upper, h1), + "verify() should be case sensitive") - def test_12_identify_unidentified(self): - "test identify() against scheme's own hashes that are mangled beyond identification" - if not self.known_unidentified_hashes: - raise self.skipTest("no unidentified hashes provided") - for hash in self.known_unidentified_hashes: - self.assertEqual(self.do_identify(hash), False, "hash=%r:" % (hash,)) + h2 = self.do_genhash(upper, h1) + if hash_insensitive or self.is_disabled_handler: + self.assertEqual(h2, h1, + "genhash() should not be case sensitive") + else: + self.assertNotEqual(h2, h1, + "genhash() should be case sensitive") - def test_13_identify_malformed(self): - "test identify() against scheme's own hashes that are mangled but identifiable" - if not self.known_malformed_hashes: - raise self.skipTest("no malformed hashes provided") - for hash in self.known_malformed_hashes: - self.assertEqual(self.do_identify(hash), True, "hash=%r:" % (hash,)) + def test_62_secret_border(self): + "test non-string passwords are rejected" + hash = self.get_sample_hash()[1] - def test_14_identify_other(self): - "test identify() against other schemes' hashes" - for name, hash in self.known_other_hashes: - self.assertEqual(self.do_identify(hash), name == self.handler.name, "scheme=%r, hash=%r:" % (name, hash)) + # secret=None + self.assertRaises(TypeError, self.do_encrypt, None) + self.assertRaises(TypeError, self.do_genhash, None, hash) + self.assertRaises(TypeError, self.do_verify, None, hash) - def test_15_identify_none(self): - "test identify() against None / empty string" - self.assertEqual(self.do_identify(None), False) - self.assertEqual(self.do_identify(b('')), self.accepts_empty_hash) - self.assertEqual(self.do_identify(u''), self.accepts_empty_hash) - - #========================================================= - #verify() - #========================================================= - def test_20_verify_positive(self): - "test verify() against known-correct secret/hash pairs" + # secret=int (picked as example of entirely wrong class) + self.assertRaises(TypeError, self.do_encrypt, 1) + self.assertRaises(TypeError, self.do_genhash, 1, hash) + self.assertRaises(TypeError, self.do_verify, 1, hash) + + def test_63_large_secret(self): + "test MAX_PASSWORD_SIZE is enforced" + from passlib.exc import PasswordSizeError + from passlib.utils import MAX_PASSWORD_SIZE + secret = '.' * (1+MAX_PASSWORD_SIZE) + hash = self.get_sample_hash()[1] + self.assertRaises(PasswordSizeError, self.do_genhash, secret, hash) + self.assertRaises(PasswordSizeError, self.do_encrypt, secret) + self.assertRaises(PasswordSizeError, self.do_verify, secret, hash) + + def test_64_forbidden_chars(self): + "test forbidden characters not allowed in password" + chars = self.forbidden_characters + if not chars: + raise self.skipTest("none listed") + base = u('stub') + if isinstance(chars, bytes): + from passlib.utils.compat import iter_byte_chars + chars = iter_byte_chars(chars) + base = base.encode("ascii") + for c in chars: + self.assertRaises(ValueError, self.do_encrypt, base + c + base) + + #=================================================================== + # check identify(), verify(), genhash() against test vectors + #=================================================================== + def is_secret_8bit(self, secret): + secret = self.populate_context(secret, {}) + return not is_ascii_safe(secret) + + def test_70_hashes(self): + "test known hashes" + # sanity check self.assertTrue(self.known_correct_hashes or self.known_correct_configs, - "test must define at least one of known_correct_hashes or known_correct_configs") + "test must set at least one of 'known_correct_hashes' " + "or 'known_correct_configs'") + + # run through known secret/hash pairs + saw8bit = False + for secret, hash in self.iter_known_hashes(): + if self.is_secret_8bit(secret): + saw8bit = True + + # hash should be positively identified by handler + self.assertTrue(self.do_identify(hash), + "identify() failed to identify hash: %r" % (hash,)) + + # secret should verify successfully against hash + self.check_verify(secret, hash, "verify() of known hash failed: " + "secret=%r, hash=%r" % (secret, hash)) + + # genhash() should reproduce same hash + result = self.do_genhash(secret, hash) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + self.assertEqual(result, hash, "genhash() failed to reproduce " + "known hash: secret=%r, hash=%r: result=%r" % + (secret, hash, result)) + + # would really like all handlers to have at least one 8-bit test vector + if not saw8bit: + warn("%s: no 8-bit secrets tested" % self.__class__) + + def test_71_alternates(self): + "test known alternate hashes" + if not self.known_alternate_hashes: + raise self.skipTest("no alternate hashes provided") + + for alt, secret, hash in self.known_alternate_hashes: + + # hash should be positively identified by handler + self.assertTrue(self.do_identify(hash), + "identify() failed to identify alternate hash: %r" % + (hash,)) + + # secret should verify successfully against hash + self.check_verify(secret, alt, "verify() of known alternate hash " + "failed: secret=%r, hash=%r" % (secret, alt)) + + # genhash() should reproduce canonical hash + result = self.do_genhash(secret, alt) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + self.assertEqual(result, hash, "genhash() failed to normalize " + "known alternate hash: secret=%r, alt=%r, hash=%r: " + "result=%r" % (secret, alt, hash, result)) + + def test_72_configs(self): + "test known config strings" + # special-case handlers without settings + if not self.handler.setting_kwds: + self.assertFalse(self.known_correct_configs, + "handler should not have config strings") + raise self.skipTest("hash has no settings") - for secret, hash in self.known_correct_hashes: - self.assertEqual(self.do_verify(secret, hash), True, - "known correct hash (secret=%r, hash=%r):" % (secret,hash)) + if not self.known_correct_configs: + # XXX: make this a requirement? + raise self.skipTest("no config strings provided") + # make sure config strings work (hashes in list tested in test_70) + if self.filter_config_warnings: + warnings.filterwarnings("ignore", category=PasslibHashWarning) for config, secret, hash in self.known_correct_configs: - self.assertEqual(self.do_verify(secret, hash), True, - "known correct hash (secret=%r, hash=%r):" % (secret,hash)) - def test_21_verify_other(self): - "test verify() throws error against other algorithm's hashes" - for name, hash in self.known_other_hashes: - if name == self.handler.name: - continue - self.assertRaises(ValueError, self.do_verify, 'fakesecret', hash, __msg__="scheme=%r, hash=%r:" % (name, hash)) + # config should be positively identified by handler + self.assertTrue(self.do_identify(config), + "identify() failed to identify known config string: %r" % + (config,)) + + # verify() should throw error for config strings. + self.assertRaises(ValueError, self.do_verify, secret, config, + __msg__="verify() failed to reject config string: %r" % + (config,)) - def test_22_verify_unidentified(self): - "test verify() throws error against known-unidentified hashes" + # genhash() should reproduce hash from config. + result = self.do_genhash(secret, config) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) + self.assertEqual(result, hash, "genhash() failed to reproduce " + "known hash from config: secret=%r, config=%r, hash=%r: " + "result=%r" % (secret, config, hash, result)) + + def test_73_unidentified(self): + "test known unidentifiably-mangled strings" if not self.known_unidentified_hashes: raise self.skipTest("no unidentified hashes provided") for hash in self.known_unidentified_hashes: - self.assertRaises(ValueError, self.do_verify, 'stub', hash, __msg__="hash=%r:" % (hash,)) - def test_23_verify_malformed(self): - "test verify() throws error against known-malformed hashes" + # identify() should reject these + self.assertFalse(self.do_identify(hash), + "identify() incorrectly identified known unidentifiable " + "hash: %r" % (hash,)) + + # verify() should throw error + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__= "verify() failed to throw error for unidentifiable " + "hash: %r" % (hash,)) + + # genhash() should throw error + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__= "genhash() failed to throw error for unidentifiable " + "hash: %r" % (hash,)) + + def test_74_malformed(self): + "test known identifiable-but-malformed strings" if not self.known_malformed_hashes: raise self.skipTest("no malformed hashes provided") for hash in self.known_malformed_hashes: - self.assertRaises(ValueError, self.do_verify, 'stub', hash, __msg__="hash=%r:" % (hash,)) - def test_24_verify_none(self): - "test verify() throws error against hash=None/empty string" - #find valid hash so that doesn't mask error - self.assertRaises(ValueError, self.do_verify, 'stub', None, __msg__="hash=None:") - if self.accepts_empty_hash: - self.do_verify("stub", u"") - self.do_verify("stub", b("")) - else: - self.assertRaises(ValueError, self.do_verify, 'stub', u'', __msg__="hash='':") - self.assertRaises(ValueError, self.do_verify, 'stub', b(''), __msg__="hash='':") - - #========================================================= - #genconfig() - #========================================================= - def test_30_genconfig_salt(self): - "test genconfig() generates new salt" - if 'salt' not in self.handler.setting_kwds: - raise self.skipTest("handler doesn't have salt") - c1 = self.do_genconfig() - c2 = self.do_genconfig() - self.assertIsInstance(c1, native_str, "genconfig() must return native str:") - self.assertIsInstance(c2, native_str, "genconfig() must return native str:") - self.assertNotEqual(c1,c2) + # identify() should accept these + self.assertTrue(self.do_identify(hash), + "identify() failed to identify known malformed " + "hash: %r" % (hash,)) + + # verify() should throw error + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__= "verify() failed to throw error for malformed " + "hash: %r" % (hash,)) + + # genhash() should throw error + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__= "genhash() failed to throw error for malformed " + "hash: %r" % (hash,)) + + def test_75_foreign(self): + "test known foreign hashes" + if self.accepts_all_hashes: + raise self.skipTest("not applicable") + if not self.known_other_hashes: + raise self.skipTest("no foreign hashes provided") + for name, hash in self.known_other_hashes: + # NOTE: most tests use default list of foreign hashes, + # so they may include ones belonging to that hash... + # hence the 'own' logic. - def test_31_genconfig_minsalt(self): - "test genconfig() honors min salt chars" - handler = self.handler - if not has_salt_info(handler): - raise self.skipTest("handler doesn't provide salt info") - cs = handler.salt_chars - cc = cs[0:1] - mn = handler.min_salt_size - c1 = self.do_genconfig(salt=cc * mn) - if mn > 0: - self.assertRaises(ValueError, self.do_genconfig, salt=cc*(mn-1)) + if name == self.handler.name: + # identify should accept these + self.assertTrue(self.do_identify(hash), + "identify() failed to identify known hash: %r" % (hash,)) + + # verify & genhash should NOT throw error + self.do_verify('stub', hash) + result = self.do_genhash('stub', hash) + self.assertIsInstance(result, str, + "genhash() failed to return native string: %r" % (result,)) - def test_32_genconfig_maxsalt(self): - "test genconfig() honors max salt chars" - handler = self.handler - if not has_salt_info(handler): - raise self.skipTest("handler doesn't provide salt info") - cs = handler.salt_chars - cc = cs[0:1] - mx = handler.max_salt_size - if mx is None: - #make sure salt is NOT truncated, - #use a really large salt for testing - salt = cc * 1024 - c1 = self.do_genconfig(salt=salt) - c2 = self.do_genconfig(salt=salt + cc) - self.assertNotEqual(c1,c2) - else: - #make sure salt is truncated exactly where it should be. - salt = cc * mx - c1 = self.do_genconfig(salt=salt) - c2 = self.do_genconfig(salt=salt + cc) - self.assertEqual(c1,c2) - - #if min_salt supports it, check smaller than mx is NOT truncated - if handler.min_salt_size < mx: - c3 = self.do_genconfig(salt=salt[:-1]) - self.assertNotEqual(c1,c3) + else: + # identify should reject these + self.assertFalse(self.do_identify(hash), + "identify() incorrectly identified hash belonging to " + "%s: %r" % (name, hash)) + + # verify should throw error + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__= "verify() failed to throw error for hash " + "belonging to %s: %r" % (name, hash,)) + + # genhash() should throw error + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__= "genhash() failed to throw error for hash " + "belonging to %s: %r" % (name, hash)) + + def test_76_hash_border(self): + "test non-string hashes are rejected" + # + # test hash=None is rejected (except if config=None) + # + self.assertRaises(TypeError, self.do_identify, None) + self.assertRaises(TypeError, self.do_verify, 'stub', None) + if self.supports_config_string: + self.assertRaises(TypeError, self.do_genhash, 'stub', None) + else: + result = self.do_genhash('stub', None) + self.check_returned_native_str(result, "genhash") - def test_33_genconfig_saltchars(self): - "test genconfig() honors salt_chars" - handler = self.handler - if not has_salt_info(handler): - raise self.skipTest("handler doesn't provide salt info") - mx = handler.max_salt_size - mn = handler.min_salt_size - cs = handler.salt_chars - raw = isinstance(cs, bytes) + # + # test hash=int is rejected (picked as example of entirely wrong type) + # + self.assertRaises(TypeError, self.do_identify, 1) + self.assertRaises(TypeError, self.do_verify, 'stub', 1) + self.assertRaises(TypeError, self.do_genhash, 'stub', 1) + + # + # test hash='' is rejected for all but the plaintext hashes + # + for hash in [u(''), b('')]: + if self.accepts_all_hashes: + # then it accepts empty string as well. + self.assertTrue(self.do_identify(hash)) + self.do_verify('stub', hash) + result = self.do_genhash('stub', hash) + self.check_returned_native_str(result, "genhash") + else: + # otherwise it should reject them + self.assertFalse(self.do_identify(hash), + "identify() incorrectly identified empty hash") + self.assertRaises(ValueError, self.do_verify, 'stub', hash, + __msg__="verify() failed to reject empty hash") + self.assertRaises(ValueError, self.do_genhash, 'stub', hash, + __msg__="genhash() failed to reject empty hash") + + # + # test identify doesn't throw decoding errors on 8-bit input + # + self.do_identify('\xe2\x82\xac\xc2\xa5$') # utf-8 + self.do_identify('abc\x91\x00') # non-utf8 + + #=================================================================== + # fuzz testing + #=================================================================== + def test_77_fuzz_input(self): + """test random passwords and options + + This test attempts to perform some basic fuzz testing of the hash, + based on whatever information can be found about it. + It does as much as it can within a fixed amount of time + (defaults to 1 second, but can be overridden via $PASSLIB_TEST_FUZZ_TIME). + It tests the following: + + * randomly generated passwords including extended unicode chars + * randomly selected rounds values (if rounds supported) + * randomly selected salt sizes (if salts supported) + * randomly selected identifiers (if multiple found) + * runs output of selected backend against other available backends + (if any) to detect errors occurring between different backends. + * runs output against other "external" verifiers such as OS crypt() + """ + if self.is_disabled_handler: + raise self.skipTest("not applicable") - #make sure all listed chars are accepted - chunk = 32 if mx is None else mx - for i in xrange(0,len(cs),chunk): - salt = cs[i:i+chunk] - if len(salt) < mn: - salt = (salt*(mn//len(salt)+1))[:chunk] - self.do_genconfig(salt=salt) + # gather info + from passlib.utils import tick + handler = self.handler + disabled = self.is_disabled_handler + max_time = self.max_fuzz_time + if max_time <= 0: + raise self.skipTest("disabled by test mode") + verifiers = self.get_fuzz_verifiers() + def vname(v): + return (v.__doc__ or v.__name__).splitlines()[0] + + # do as many tests as possible for max_time seconds + stop = tick() + max_time + count = 0 + while tick() <= stop: + # generate random password & options + secret, other, kwds = self.get_fuzz_settings() + ctx = dict((k,kwds[k]) for k in handler.context_kwds if k in kwds) + + # create new hash + hash = self.do_encrypt(secret, **kwds) + ##log.debug("fuzz test: hash=%r secret=%r other=%r", + ## hash, secret, other) + + # run through all verifiers we found. + for verify in verifiers: + name = vname(verify) + result = verify(secret, hash, **ctx) + if result == "skip": # let verifiers signal lack of support + continue + assert result is True or result is False + if not result: + raise self.failureException("failed to verify against %s: " + "secret=%r config=%r hash=%r" % + (name, secret, kwds, hash)) + # occasionally check that some other secrets WON'T verify + # against this hash. + if rng.random() < .1: + result = verify(other, hash, **ctx) + if result and result != "skip": + raise self.failureException("was able to verify wrong " + "password using %s: wrong_secret=%r real_secret=%r " + "config=%r hash=%r" % (name, other, secret, kwds, hash)) + count +=1 + + log.debug("fuzz test: %r checked %d passwords against %d verifiers (%s)", + self.descriptionPrefix, count, len(verifiers), + ", ".join(vname(v) for v in verifiers)) + + #--------------------------------------------------------------- + # fuzz constants & helpers + #--------------------------------------------------------------- + + # alphabet for randomly generated passwords + fuzz_password_alphabet = u('qwertyASDF1234<>.@*#! \u00E1\u0259\u0411\u2113') + + # encoding when testing bytes + fuzz_password_encoding = "utf-8" + + @property + def max_fuzz_time(self): + "amount of time to spend on fuzz testing" + value = float(os.environ.get("PASSLIB_TEST_FUZZ_TIME") or 0) + if value: + return value + elif TEST_MODE(max="quick"): + return 0 + elif TEST_MODE(max="default"): + return 1 + else: + return 5 - #check some invalid salt chars, make sure they're rejected - source = u'\x00\xff' - if raw: - source = source.encode("latin-1") - chunk = max(mn, 1) - for c in source: - if c not in cs: - self.assertRaises(ValueError, self.do_genconfig, salt=c*chunk, __msg__="invalid salt char %r:" % (c,)) + def os_supports_ident(self, ident): + "whether native OS crypt() supports particular ident value" + return True + + #--------------------------------------------------------------- + # fuzz verifiers + #--------------------------------------------------------------- + def get_fuzz_verifiers(self): + """return list of password verifiers (including external libs) + + used by fuzz testing. + verifiers should be callable with signature + ``func(password: unicode, hash: ascii str) -> ok: bool``. + """ + handler = self.handler + verifiers = [] - #========================================================= - #genhash() - #========================================================= - filter_known_config_warnings = None + # call all methods starting with prefix in order to create + # any verifiers. + prefix = "fuzz_verifier_" + for name in dir(self): + if name.startswith(prefix): + func = getattr(self, name)() + if func is not None: + verifiers.append(func) + + # create verifiers for any other available backends + if hasattr(handler, "backends") and TEST_MODE("full"): + def maker(backend): + def func(secret, hash): + with temporary_backend(handler, backend): + return handler.verify(secret, hash) + func.__name__ = "check_" + backend + "_backend" + func.__doc__ = backend + "-backend" + return func + cur = handler.get_backend() + for backend in handler.backends: + if backend != cur and handler.has_backend(backend): + verifiers.append(maker(backend)) + + return verifiers + + def fuzz_verifier_default(self): + # test against self + def check_default(secret, hash, **ctx): + return self.do_verify(secret, hash, **ctx) + if self.backend: + check_default.__doc__ = self.backend + "-backend" + else: + check_default.__doc__ = "self" + return check_default - def test_40_genhash_config(self): - "test genhash() against known config strings" - if not self.known_correct_configs: - raise self.skipTest("no config strings provided") - fk = self.filter_known_config_warnings - if fk: - ctx = catch_warnings() - ctx.__enter__() - fk() - for config, secret, hash in self.known_correct_configs: - result = self.do_genhash(secret, config) - self.assertEqual(result, hash, "config=%r,secret=%r:" % (config,secret)) - if fk: - ctx.__exit__(None,None,None) - - def test_41_genhash_hash(self): - "test genhash() against known hash strings" - if not self.known_correct_hashes: - raise self.skipTest("no correct hashes provided") + def fuzz_verifier_crypt(self): + "test results against OS crypt()" handler = self.handler - for secret, hash in self.known_correct_hashes: - result = self.do_genhash(secret, hash) - self.assertEqual(result, hash, "secret=%r:" % (secret,)) + if self.using_patched_crypt or not has_crypt_support(handler): + return None + from crypt import crypt + def check_crypt(secret, hash): + "stdlib-crypt" + if not self.os_supports_ident(hash): + return "skip" + secret = to_native_str(secret, self.fuzz_password_encoding) + return crypt(secret, hash) == hash + return check_crypt + + #--------------------------------------------------------------- + # fuzz settings generation + #--------------------------------------------------------------- + def get_fuzz_settings(self): + "generate random password and options for fuzz testing" + prefix = "fuzz_setting_" + kwds = {} + for name in dir(self): + if name.startswith(prefix): + value = getattr(self, name)() + if value is not None: + kwds[name[len(prefix):]] = value + secret, other = self.get_fuzz_password_pair() + return secret, other, kwds - def test_42_genhash_genconfig(self): - "test genhash() against genconfig() output" + def fuzz_setting_rounds(self): handler = self.handler - config = handler.genconfig() - hash = self.do_genhash("stub", config) - self.assertTrue(handler.identify(hash)) + if not has_rounds_info(handler): + return None + default = handler.default_rounds or handler.min_rounds + lower = handler.min_rounds + if handler.rounds_cost == "log2": + upper = default + else: + upper = min(default*2, handler.max_rounds) + return randintgauss(lower, upper, default, default*.5) - def test_43_genhash_none(self): - "test genhash() against hash=None" + def fuzz_setting_salt_size(self): handler = self.handler - config = handler.genconfig() - if config is None: - raise self.skipTest("handler doesnt use config strings") - self.assertRaises(ValueError, handler.genhash, 'secret', None) - - #========================================================= - #encrypt() - #========================================================= - def test_50_encrypt_plain(self): - "test encrypt() basic behavior" - #check it handles unicode password - secret = u"\u20AC\u00A5$" - result = self.do_encrypt(secret) - self.assertIsInstance(result, native_str, "encrypt must return native str:") - self.assertTrue(self.do_identify(result)) - self.assertTrue(self.do_verify(secret, result)) - - #check it handles bytes password as well - secret = b('\xe2\x82\xac\xc2\xa5$') - result = self.do_encrypt(secret) - self.assertIsInstance(result, native_str, "encrypt must return native str:") - self.assertTrue(self.do_identify(result)) - self.assertTrue(self.do_verify(secret, result)) - - def test_51_encrypt_none(self): - "test encrypt() refused secret=None" - self.assertRaises(TypeError, self.do_encrypt, None) - - def test_52_encrypt_salt(self): - "test encrypt() generates new salt" - if 'salt' not in self.handler.setting_kwds: - raise self.skipTest("handler doesn't have salt") - #test encrypt() - h1 = self.do_encrypt("stub") - h2 = self.do_encrypt("stub") - self.assertNotEqual(h1, h2) - - # optional helper used by test_53_external_verifiers - iter_external_verifiers = None - - def test_53_external_verifiers(self): - "test encrypt() output verifies against external libs" - # this makes sure our output can be verified by external libs, - # to avoid repeat of things like issue 25. + if not (has_salt_info(handler) and 'salt_size' in handler.setting_kwds): + return None + default = handler.default_salt_size + lower = handler.min_salt_size + upper = handler.max_salt_size or default*4 + return randintgauss(lower, upper, default, default*.5) + def fuzz_setting_ident(self): handler = self.handler - possible = False - if self.iter_external_verifiers: - helpers = list(self.iter_external_verifiers()) - possible = True - else: - helpers = [] - - # provide default "os_crypt" helper - if hasattr(handler, "has_backend") and \ - 'os_crypt' in handler.backends and \ - not hasattr(handler, "orig_prefix"): - possible = True - if handler.has_backend("os_crypt"): - def check_crypt(secret, hash): - self.assertEqual(utils.os_crypt(secret, hash), hash, - "os_crypt(%r,%r):" % (secret, hash)) - helpers.append(check_crypt) - - if not helpers: - if possible: - raise self.skipTest("no external libs available") - else: - raise self.skipTest("not applicable") - - # generate a single hash, and verify it using all helpers. - secret = 't\xc3\xa1\xd0\x91\xe2\x84\x93\xc9\x99' - hash = self.do_encrypt(secret) - for helper in helpers: - helper(secret, hash) - - #========================================================= - #test max password size - #========================================================= - def test_60_secret_chars(self): - "test secret_chars limit" - sc = self.secret_chars - - base = "too many secrets" #16 chars - alt = 'x' #char that's not in base string - - if sc > 0: - #hash only counts the first characters - #eg: bcrypt, des-crypt - - #create & hash something of exactly sc+1 chars - secret = (base * (1+sc//16))[:sc+1] - assert len(secret) == sc+1 - hash = self.do_encrypt(secret) - - #check sc value isn't too large - #by verifying that sc-1'th char affects hash - self.assertTrue(not self.do_verify(secret[:-2] + alt + secret[-1], hash), "secret_chars value is too large") - - #check sc value isn't too small - #by verifying adding sc'th char doesn't affect hash - self.assertTrue(self.do_verify(secret[:-1] + alt, hash)) - - else: - #hash counts all characters - #eg: md5-crypt - self.assertEqual(sc, -1) - - #NOTE: this doesn't do an exhaustive search to verify algorithm - #doesn't have some cutoff point, it just tries - #1024-character string, and alters the last char. - #as long as algorithm doesn't clip secret at point <1024, - #the new secret shouldn't verify. - secret = base * 64 - hash = self.do_encrypt(secret) - self.assertTrue(not self.do_verify(secret[:-1] + alt, hash)) + if 'ident' not in handler.setting_kwds or not hasattr(handler, "ident_values"): + return None + if rng.random() < .5: + return None + # resolve wrappers before reading values + handler = getattr(handler, "wrapped", handler) + ident = rng.choice(handler.ident_values) + if self.backend == "os_crypt" and not self.using_patched_crypt and not self.os_supports_ident(ident): + return None + return ident - #========================================================= - #eoc - #========================================================= - -#========================================================= -#backend test helpers -#========================================================= -def _enable_backend_case(handler, backend): - "helper to check if testcase should be enabled for the specified backend" - assert backend in handler.backends, "unknown backend: %r" % (backend,) - if enable_option("all-backends") or _is_default_backend(handler, backend): - if handler.has_backend(backend): - return True, None - if backend == "os_crypt" and utils.safe_os_crypt: - if enable_option("cover") and _has_other_backends(handler, "os_crypt"): - #in this case, HandlerCase will monkeypatch os_crypt - #to use another backend, just so we can test os_crypt fully. - return True, None - else: - return False, "hash not supported by os crypt()" + #--------------------------------------------------------------- + # fuzz password generation + #--------------------------------------------------------------- + def get_fuzz_password(self): + "generate random passwords for fuzz testing" + # occasionally try an empty password + if rng.random() < .0001: + return u('') + # otherwise alternate between large and small passwords. + if rng.random() < .5: + size = randintgauss(1, 50, 15, 15) else: - return False, "backend not available" - else: - return False, "only default backend being tested" - -def _is_default_backend(handler, name): - "check if backend is the default for handler" - try: - orig = handler.get_backend() - except MissingBackendError: - return False - try: - return handler.set_backend("default") == name - finally: - handler.set_backend(orig) - -def _has_other_backends(handler, ignore): - "helper to check if alternate backend is available" - for name in handler.backends: - if name != ignore and handler.has_backend(name): - return name - return None - -def create_backend_case(base, name, module="passlib.tests.test_drivers"): - "create a test case for specific backend of a multi-backend handler" - #get handler, figure out if backend should be tested - handler = base.handler - assert hasattr(handler, "backends"), "handler must support uh.HasManyBackends protocol" - enable, reason = _enable_backend_case(handler, name) - - #UT1 doesn't support skipping whole test cases, - #so we just return None. - if not enable and ut_version < 2: - return None - - #make classname match what it's stored under, to be tidy - cname = name.title().replace("_","") + "_" + base.__name__.lstrip("_") - - #create subclass of 'base' which uses correct backend - subcase = type( - cname, - (base,), - dict( - case_prefix = "%s (%s backend)" % (handler.name, name), - backend = name, - __module__=module, - ) - ) - - if not enable: - subcase = unittest.skip(reason)(subcase) - - return subcase - -#========================================================= -#misc helpers -#========================================================= -class dummy_handler_in_registry(object): - "context manager that inserts dummy handler in registry" - def __init__(self, name): - self.name = name - self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict( - name=name, - setting_kwds=(), - )) + size = randintgauss(50, 99, 70, 20) + return getrandstr(rng, self.fuzz_password_alphabet, size) - def __enter__(self): - registry._unload_handler_name(self.name, locations=False) - registry.register_crypt_handler(self.dummy) - assert registry.get_crypt_handler(self.name) is self.dummy - return self.dummy - - def __exit__(self, *exc_info): - registry._unload_handler_name(self.name, locations=False) - -#========================================================= -#helper for creating temp files - all cleaned up when prog exits -#========================================================= -tmp_files = [] - -def _clean_tmp_files(): - for path in tmp_files: - if os.path.exists(path): - os.remove(path) -atexit.register(_clean_tmp_files) - -def mktemp(*args, **kwds): - fd, path = tempfile.mkstemp(*args, **kwds) - tmp_files.append(path) - os.close(fd) - return path - -#========================================================= -#make sure catch_warnings() is available -#========================================================= -try: - from warnings import catch_warnings -except ImportError: - #catch_warnings wasn't added until py26. - #this adds backported copy from py26's stdlib - #so we can use it under py25. - - class WarningMessage(object): + def accept_fuzz_pair(self, secret, other): + "verify fuzz pair contains different passwords" + return secret != other + + def get_fuzz_password_pair(self): + "generate random password, and non-matching alternate password" + secret = self.get_fuzz_password() + while True: + other = self.get_fuzz_password() + if self.accept_fuzz_pair(secret, other): + break + if rng.randint(0,1): + secret = secret.encode(self.fuzz_password_encoding) + if rng.randint(0,1): + other = other.encode(self.fuzz_password_encoding) + return secret, other + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# HandlerCase mixins providing additional tests for certain hashes +#============================================================================= +class OsCryptMixin(HandlerCase): + """helper used by create_backend_case() which adds additional features + to test the os_crypt backend. - """Holds the result of a single showwarning() call.""" + * if crypt support is missing, inserts fake crypt support to simulate + a working safe_crypt, to test passlib's codepath as fully as possible. - _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file", - "line") + * extra tests to verify non-conformant crypt implementations are handled + correctly. - def __init__(self, message, category, filename, lineno, file=None, - line=None): - local_values = locals() - for attr in self._WARNING_DETAILS: - setattr(self, attr, local_values[attr]) - self._category_name = category.__name__ if category else None + * check that native crypt support is detected correctly for known platforms. + """ + #=================================================================== + # option flags + #=================================================================== + # platforms that are known to support / not support this hash natively. + # list of (platform_regex, True|False|None) entries. + platform_crypt_support = [] + + #=================================================================== + # instance attrs + #=================================================================== + __unittest_skip = True + + # force this backend + backend = "os_crypt" + + # flag read by HandlerCase to detect if fake os crypt is enabled. + using_patched_crypt = False + + #=================================================================== + # setup + #=================================================================== + def setUp(self): + assert self.backend == "os_crypt" + if not self.handler.has_backend("os_crypt"): + self.handler.get_backend() # hack to prevent recursion issue + self._patch_safe_crypt() + super(OsCryptMixin, self).setUp() + + def _patch_safe_crypt(self): + """if crypt() doesn't support current hash alg, this patches + safe_crypt() so that it transparently uses another one of the handler's + backends, so that we can go ahead and test as much of code path + as possible. + """ + handler = self.handler + # resolve wrappers, since we want to return crypt compatible hash. + while hasattr(handler, "wrapped"): + handler = handler.wrapped + alt_backend = self.find_crypt_replacement() + if not alt_backend: + raise AssertionError("handler has no available backends!") + import passlib.utils as mod + def crypt_stub(secret, hash): + with temporary_backend(handler, alt_backend): + hash = handler.genhash(secret, hash) + assert isinstance(hash, str) + return hash + self.addCleanup(setattr, mod, "_crypt", mod._crypt) + mod._crypt = crypt_stub + self.using_patched_crypt = True + + #=================================================================== + # custom tests + #=================================================================== + def _use_mock_crypt(self): + "patch safe_crypt() so it returns mock value" + import passlib.utils as mod + if not self.using_patched_crypt: + self.addCleanup(setattr, mod, "_crypt", mod._crypt) + crypt_value = [None] + mod._crypt = lambda secret, config: crypt_value[0] + def setter(value): + crypt_value[0] = value + return setter + + def test_80_faulty_crypt(self): + "test with faulty crypt()" + hash = self.get_sample_hash()[1] + exc_types = (AssertionError,) + setter = self._use_mock_crypt() + + def test(value): + # set safe_crypt() to return specified value, and + # make sure assertion error is raised by handler. + setter(value) + self.assertRaises(exc_types, self.do_genhash, "stub", hash) + self.assertRaises(exc_types, self.do_encrypt, "stub") + self.assertRaises(exc_types, self.do_verify, "stub", hash) + + test('$x' + hash[2:]) # detect wrong prefix + test(hash[:-1]) # detect too short + test(hash + 'x') # detect too long + + def test_81_crypt_fallback(self): + "test per-call crypt() fallback" + # set safe_crypt to return None + setter = self._use_mock_crypt() + setter(None) + if self.find_crypt_replacement(): + # handler should have a fallback to use + h1 = self.do_encrypt("stub") + h2 = self.do_genhash("stub", h1) + self.assertEqual(h2, h1) + self.assertTrue(self.do_verify("stub", h1)) + else: + # handler should give up + from passlib.exc import MissingBackendError + hash = self.get_sample_hash()[1] + self.assertRaises(MissingBackendError, self.do_encrypt, 'stub') + self.assertRaises(MissingBackendError, self.do_genhash, 'stub', hash) + self.assertRaises(MissingBackendError, self.do_verify, 'stub', hash) + + def test_82_crypt_support(self): + "test platform-specific crypt() support detection" + # NOTE: this is mainly just a sanity check to ensure the runtime + # detection is functioning correctly on some known platforms, + # so that I can feel more confident it'll work right on unknown ones. + if hasattr(self.handler, "orig_prefix"): + raise self.skipTest("not applicable to wrappers") + platform = sys.platform + for pattern, state in self.platform_crypt_support: + if re.match(pattern, platform): + break + else: + raise self.skipTest("no data for %r platform" % platform) + if state is None: + # e.g. platform='freebsd8' ... sha256_crypt not added until 8.3 + raise self.skipTest("varied support on %r platform" % platform) + elif state != self.using_patched_crypt: + return + elif state: + self.fail("expected %r platform would have native support " + "for %r" % (platform, self.handler.name)) + else: + self.fail("did not expect %r platform would have native support " + "for %r" % (platform, self.handler.name)) - def __str__(self): - return ("{message : %r, category : %r, filename : %r, lineno : %s, " - "line : %r}" % (self.message, self._category_name, - self.filename, self.lineno, self.line)) + #=================================================================== + # eoc + #=================================================================== + +class UserHandlerMixin(HandlerCase): + """helper for handlers w/ 'user' context kwd; mixin for HandlerCase + + this overrides the HandlerCase test harness methods + so that a username is automatically inserted to encrypt/verify + calls. as well, passing in a pair of strings as the password + will be interpreted as (secret,user) + """ + #=================================================================== + # option flags + #=================================================================== + default_user = "user" + requires_user = True + user_case_insensitive = False + + #=================================================================== + # instance attrs + #=================================================================== + __unittest_skip = True + + #=================================================================== + # custom tests + #=================================================================== + def test_80_user(self): + "test user context keyword" + handler = self.handler + password = 'stub' + hash = handler.encrypt(password, user=self.default_user) + if self.requires_user: + self.assertRaises(TypeError, handler.encrypt, password) + self.assertRaises(TypeError, handler.genhash, password, hash) + self.assertRaises(TypeError, handler.verify, password, hash) + else: + # e.g. cisco_pix works with or without one. + handler.encrypt(password) + handler.genhash(password, hash) + handler.verify(password, hash) + + def test_81_user_case(self): + "test user case sensitivity" + lower = self.default_user.lower() + upper = lower.upper() + hash = self.do_encrypt('stub', user=lower) + if self.user_case_insensitive: + self.assertTrue(self.do_verify('stub', hash, user=upper), + "user should not be case sensitive") + else: + self.assertFalse(self.do_verify('stub', hash, user=upper), + "user should be case sensitive") - class catch_warnings(object): + def test_82_user_salt(self): + "test user used as salt" + config = self.do_genconfig() + h1 = self.do_genhash('stub', config, user='admin') + h2 = self.do_genhash('stub', config, user='admin') + self.assertEqual(h2, h1) + h3 = self.do_genhash('stub', config, user='root') + self.assertNotEqual(h3, h1) + + # TODO: user size? kinda dicey, depends on algorithm. + + #=================================================================== + # override test helpers + #=================================================================== + def populate_context(self, secret, kwds): + "insert username into kwds" + if isinstance(secret, tuple): + secret, user = secret + elif not self.requires_user: + return secret + else: + user = self.default_user + if 'user' not in kwds: + kwds['user'] = user + return secret + + #=================================================================== + # modify fuzz testing + #=================================================================== + fuzz_user_alphabet = u("asdQWE123") - """A context manager that copies and restores the warnings filter upon - exiting the context. + def fuzz_setting_user(self): + if not self.requires_user and rng.random() < .1: + return None + return getrandstr(rng, self.fuzz_user_alphabet, rng.randint(2,10)) - The 'record' argument specifies whether warnings should be captured by a - custom implementation of warnings.showwarning() and be appended to a list - returned by the context manager. Otherwise None is returned by the context - manager. The objects appended to the list are arguments whose attributes - mirror the arguments to showwarning(). + #=================================================================== + # eoc + #=================================================================== + +class EncodingHandlerMixin(HandlerCase): + """helper for handlers w/ 'encoding' context kwd; mixin for HandlerCase + + this overrides the HandlerCase test harness methods + so that an encoding can be inserted to encrypt/verify + calls by passing in a pair of strings as the password + will be interpreted as (secret,encoding) + """ + #=================================================================== + # instance attrs + #=================================================================== + __unittest_skip = True + + # restrict stock passwords & fuzz alphabet to latin-1, + # so different encodings can be tested safely. + stock_passwords = [ + u("test"), + b("test"), + u("\u00AC\u00BA"), + ] - The 'module' argument is to specify an alternative module to the module - named 'warnings' and imported under that name. This argument is only useful - when testing the warnings module itself. + fuzz_password_alphabet = u('qwerty1234<>.@*#! \u00AC') - """ + def populate_context(self, secret, kwds): + "insert encoding into kwds" + if isinstance(secret, tuple): + secret, encoding = secret + kwds.setdefault('encoding', encoding) + return secret + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# warnings helpers +#============================================================================= +class reset_warnings(catch_warnings): + """catch_warnings() wrapper which clears warning registry & filters""" + def __init__(self, reset_filter="always", reset_registry=".*", **kwds): + super(reset_warnings, self).__init__(**kwds) + self._reset_filter = reset_filter + self._reset_registry = re.compile(reset_registry) if reset_registry else None - def __init__(self, record=False, module=None): - """Specify whether to record warnings and if an alternative module - should be used other than sys.modules['warnings']. - - For compatibility with Python 3.0, please consider all arguments to be - keyword-only. - - """ - self._record = record - self._module = sys.modules['warnings'] if module is None else module - self._entered = False - - def __repr__(self): - args = [] - if self._record: - args.append("record=True") - if self._module is not sys.modules['warnings']: - args.append("module=%r" % self._module) - name = type(self).__name__ - return "%s(%s)" % (name, ", ".join(args)) + def __enter__(self): + # let parent class archive filter state + ret = super(reset_warnings, self).__enter__() - def __enter__(self): - if self._entered: - raise RuntimeError("Cannot enter %r twice" % self) - self._entered = True - self._filters = self._module.filters - self._module.filters = self._filters[:] - self._showwarning = self._module.showwarning - if self._record: - log = [] - def showwarning(*args, **kwargs): - log.append(WarningMessage(*args, **kwargs)) - self._module.showwarning = showwarning - return log - else: - return None + # reset the filter to list everything + if self._reset_filter: + warnings.resetwarnings() + warnings.simplefilter(self._reset_filter) + + # archive and clear the __warningregistry__ key for all modules + # that match the 'reset' pattern. + pattern = self._reset_registry + if pattern: + orig = self._orig_registry = {} + for name, mod in sys.modules.items(): + if pattern.match(name): + reg = getattr(mod, "__warningregistry__", None) + if reg: + orig[name] = reg.copy() + reg.clear() + return ret - def __exit__(self, *exc_info): - if not self._entered: - raise RuntimeError("Cannot exit %r without entering first" % self) - self._module.filters = self._filters - self._module.showwarning = self._showwarning - -#========================================================= -#EOF -#========================================================= + def __exit__(self, *exc_info): + # restore warning registry for all modules + pattern = self._reset_registry + if pattern: + # restore archived registry data + orig = self._orig_registry + for name, content in iteritems(orig): + mod = sys.modules.get(name) + if mod is None: + continue + reg = getattr(mod, "__warningregistry__", None) + if reg is None: + setattr(mod, "__warningregistry__", content) + else: + reg.clear() + reg.update(content) + # clear all registry entries that we didn't archive + for name, mod in sys.modules.items(): + if pattern.match(name) and name not in orig: + reg = getattr(mod, "__warningregistry__", None) + if reg: + reg.clear() + super(reset_warnings, self).__exit__(*exc_info) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/__init__.py passlib-1.6.1/passlib/utils/__init__.py --- passlib-1.5.3/passlib/utils/__init__.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/utils/__init__.py 2012-08-01 20:59:17.000000000 +0000 @@ -1,235 +1,233 @@ -"""passlib utility functions""" -#================================================================================= -#imports -#================================================================================= -#core +"""passlib.utils -- helpers for writing password hashes""" +#============================================================================= +# imports +#============================================================================= +from passlib.utils.compat import PYPY, JYTHON +# core from base64 import b64encode, b64decode from codecs import lookup as _lookup_codec -from cStringIO import StringIO -##from functools import update_wrapper -from hashlib import sha256 +from functools import update_wrapper import logging; log = logging.getLogger(__name__) -from math import log as logb +import math import os import sys import random +if JYTHON: # pragma: no cover -- runtime detection + # Jython 2.5.2 lacks stringprep module - + # see http://bugs.jython.org/issue1758320 + try: + import stringprep + except ImportError: + stringprep = None + _stringprep_missing_reason = "not present under Jython" +else: + import stringprep import time +if stringprep: + import unicodedata from warnings import warn -#site -#pkg -#local +# site +# pkg +from passlib.exc import ExpectedStringError +from passlib.utils.compat import add_doc, b, bytes, join_bytes, join_byte_values, \ + join_byte_elems, exc_err, irange, imap, PY3, u, \ + join_unicode, unicode, byte_elem_value, PY_MIN_32, next_method_attr +# local __all__ = [ - #decorators + # constants + 'PYPY', + 'JYTHON', + 'sys_bits', + 'unix_crypt_schemes', + 'rounds_cost_values', + + # decorators "classproperty", +## "deprecated_function", +## "relocated_function", ## "memoized_class_property", -## "abstractmethod", -## "abstractclassmethod", - #byte compat aliases - 'bytes', 'native_str', + # unicode helpers + 'consteq', + 'saslprep', - #misc - 'os_crypt', - - #tests - 'is_crypt_handler', - 'is_crypt_context', + # bytes helpers + "xor_bytes", + "render_bytes", - #bytes<->unicode + # encoding helpers + 'is_same_codec', + 'is_ascii_safe', 'to_bytes', 'to_unicode', 'to_native_str', - 'is_same_codec', - #byte manipulation - "xor_bytes", + # base64 helpers + "BASE64_CHARS", "HASH64_CHARS", "BCRYPT_CHARS", "AB64_CHARS", + "Base64Engine", "h64", "h64big", + "ab64_encode", "ab64_decode", + + # host OS + 'has_crypt', + 'test_crypt', + 'safe_crypt', + 'tick', - #random + # randomness 'rng', 'getrandbytes', 'getrandstr', + 'generate_password', - #constants - 'pypy_vm', 'jython_vm', - 'py32_lang', 'py3k_lang', - 'sys_bits', - 'unix_crypt_schemes', + # object type / interface tests + 'is_crypt_handler', + 'is_crypt_context', + 'has_rounds_info', + 'has_salt_info', ] -#================================================================================= -#constants -#================================================================================= - -#: detect what we're running on -pypy_vm = hasattr(sys, "pypy_version_info") -jython_vm = sys.platform.startswith('java') -py3k_lang = sys.version_info >= (3,0) -py32_lang = sys.version_info >= (3,2) - -#: number of bits in system architecture -sys_bits = int(logb(sys.maxint,2)+1.5) -assert sys_bits in (32,64), "unexpected sys_bits value: %r" % (sys_bits,) +#============================================================================= +# constants +#============================================================================= + +# bitsize of system architecture (32 or 64) +sys_bits = int(math.log(sys.maxsize if PY3 else sys.maxint, 2) + 1.5) -#: list of names of hashes found in unix crypt implementations... +# list of hashes algs supported by crypt() on at least one OS. unix_crypt_schemes = [ "sha512_crypt", "sha256_crypt", "sha1_crypt", "bcrypt", "md5_crypt", - "bsdi_crypt", "des_crypt" + # "bsd_nthash", + "bsdi_crypt", "des_crypt", ] -#: list of rounds_cost constants +# list of rounds_cost constants rounds_cost_values = [ "linear", "log2" ] -#: special byte string containing all possible byte values, used in a few places. -#XXX: treated as singleton by some of the code for efficiency. -# Py2k # -ALL_BYTE_VALUES = ''.join(chr(x) for x in xrange(256)) -# Py3k # -#ALL_BYTE_VALUES = bytes(xrange(256)) -# end Py3k # - -#NOTE: Undef is only used in *one* place now, could just remove it -class UndefType(object): - _undef = None - - def __new__(cls): - if cls._undef is None: - cls._undef = object.__new__(cls) - return cls._undef +# legacy import, will be removed in 1.8 +from passlib.exc import MissingBackendError - def __repr__(self): - return '' - - def __eq__(self, other): - return False - - def __ne__(self, other): - return True - -#: singleton used as default kwd value in some functions, indicating "NO VALUE" -Undef = UndefType() +# internal helpers +_BEMPTY = b('') +_UEMPTY = u("") +_USPACE = u(" ") + +# maximum password size which passlib will allow; see exc.PasswordSizeError +MAX_PASSWORD_SIZE = int(os.environ.get("PASSLIB_MAX_PASSWORD_SIZE") or 4096) + +#============================================================================= +# decorators and meta helpers +#============================================================================= +class classproperty(object): + """Function decorator which acts like a combination of classmethod+property (limited to read-only properties)""" -NoneType = type(None) + def __init__(self, func): + self.im_func = func -class MissingBackendError(RuntimeError): - """error raised if multi-backend handler has no available backends; - or if specifically requested backend is not available. + def __get__(self, obj, cls): + return self.im_func(cls) - see :class:`~passlib.utils.handlers.HasManyBackends`. + @property + def __func__(self): + "py3 compatible alias" + return self.im_func + +def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True, + replacement=None, _is_method=False): + """decorator to deprecate a function. + + :arg msg: optional msg, default chosen if omitted + :kwd deprecated: version when function was first deprecated + :kwd removed: version when function will be removed + :kwd replacement: alternate name / instructions for replacing this function. + :kwd updoc: add notice to docstring (default ``True``) """ + if msg is None: + if _is_method: + msg = "the method %(mod)s.%(klass)s.%(name)s() is deprecated" + else: + msg = "the function %(mod)s.%(name)s() is deprecated" + if deprecated: + msg += " as of Passlib %(deprecated)s" + if removed: + msg += ", and will be removed in Passlib %(removed)s" + if replacement: + msg += ", use %s instead" % replacement + msg += "." + def build(func): + opts = dict( + mod=func.__module__, + name=func.__name__, + deprecated=deprecated, + removed=removed, + ) + if _is_method: + def wrapper(*args, **kwds): + tmp = opts.copy() + klass = args[0].__class__ + tmp.update(klass=klass.__name__, mod=klass.__module__) + warn(msg % tmp, DeprecationWarning, stacklevel=2) + return func(*args, **kwds) + else: + text = msg % opts + def wrapper(*args, **kwds): + warn(text, DeprecationWarning, stacklevel=2) + return func(*args, **kwds) + update_wrapper(wrapper, func) + if updoc and (deprecated or removed) and wrapper.__doc__: + txt = deprecated or '' + if removed or replacement: + txt += "\n " + if removed: + txt += "and will be removed in version %s" % (removed,) + if replacement: + if removed: + txt += ", " + txt += "use %s instead" % replacement + txt += "." + if not wrapper.__doc__.strip(" ").endswith("\n"): + wrapper.__doc__ += "\n" + wrapper.__doc__ += "\n.. deprecated:: %s\n" % (txt,) + return wrapper + return build + +def deprecated_method(msg=None, deprecated=None, removed=None, updoc=True, + replacement=None): + """decorator to deprecate a method. + + :arg msg: optional msg, default chosen if omitted + :kwd deprecated: version when method was first deprecated + :kwd removed: version when method will be removed + :kwd replacement: alternate name / instructions for replacing this method. + :kwd updoc: add notice to docstring (default ``True``) + """ + return deprecated_function(msg, deprecated, removed, updoc, replacement, + _is_method=True) -#========================================================== -#bytes compat aliases - bytes, native_str, b() -#========================================================== - -# Py2k # -if sys.version_info < (2,6): - #py25 doesn't define 'bytes', so we have to here - - #and then import it everywhere bytes is needed, - #just so we retain py25 compat - if that were sacrificed, - #the need for this would go away - bytes = str -else: - bytes = bytes #just so it *can* be imported from this module -native_str = bytes -# Py3k # -#bytes = bytes #just so it *can* be imported from this module -#native_str = unicode -# end Py3k # - -#NOTE: have to provide b() because we're supporting py25, -# and py25 doesn't support the b'' notation. -# if py25 compat were sacrificed, this func could be removed. -def b(source): - "convert native str to bytes (noop under py2; uses latin-1 under py3)" - #assert isinstance(source, native_str) - # Py2k # - return source - # Py3k # - #return source.encode("latin-1") - # end Py3k # - -#================================================================================= -#os crypt helpers -#================================================================================= - -#expose crypt function as 'os_crypt', set to None if not available. -try: - from crypt import crypt as os_crypt -except ImportError: #pragma: no cover - safe_os_crypt = os_crypt = None -else: - def safe_os_crypt(secret, hash): - """wrapper around stdlib's crypt. - - Python 3's crypt behaves slightly differently from Python 2's crypt. - for one, it takes in and returns unicode. - internally, it converts to utf-8 before hashing. - Annoyingly, *there is no way to call it using bytes*. - thus, it can't be used to hash non-ascii passwords - using any encoding but utf-8 (eg, using latin-1). - - This wrapper attempts to gloss over all those issues: - Under Python 2, it accept passwords as unicode or bytes, - accepts hashes only as unicode, and always returns unicode. - Under Python 3, it will signal that it cannot hash a password - if provided as non-utf-8 bytes, but otherwise behave the same as crypt. - - :arg secret: password as bytes or unicode - :arg hash: hash/salt as unicode - :returns: - ``(False, None)`` if the password can't be hashed (3.x only), - or ``(True, result: unicode)`` otherwise. - """ - #XXX: source indicates crypt() may return None on some systems - # if an error occurrs - could make this return False in that case. - - # Py2k # - #NOTE: this guard logic is designed purely to match py3 behavior, - # with the exception that it accepts secret as bytes - if isinstance(secret, unicode): - secret = secret.encode("utf-8") - if isinstance(hash, bytes): - raise TypeError("hash must be unicode") - else: - hash = hash.encode("utf-8") - return True, os_crypt(secret, hash).decode("ascii") - - # Py3k # - #if isinstance(secret, bytes): - # #decode to utf-8. if successful, will be reencoded with os_crypt, - # #and we'll get back correct hash. - # #if not, we can't use os_crypt for this. - # orig = secret - # try: - # secret = secret.decode("utf-8") - # except UnicodeDecodeError: - # return False, None - # if secret.encode("utf-8") != orig: - # #just in case original encoding wouldn't be reproduced - # #during call to os_crypt. - # #not sure if/how this could happen, but being paranoid. - # warn("utf-8 password didn't re-encode correctly") - # return False, None - #return True, os_crypt(secret, hash) - # end Py3k # - -#================================================================================= -#decorators and meta helpers -#================================================================================= -class classproperty(object): - """Function decorator which acts like a combination of classmethod+property (limited to read-only properties)""" - +class memoized_property(object): + """decorator which invokes method once, then replaces attr with result""" def __init__(self, func): self.im_func = func def __get__(self, obj, cls): - return self.im_func(cls) + if obj is None: + return self + func = self.im_func + value = func(obj) + setattr(obj, func.__name__, value) + return value + + @property + def __func__(self): + "py3 alias" + return self.im_func -#works but not used +# works but not used ##class memoized_class_property(object): -## """function decorator which calls function as classmethod, and replaces itself with result for current and all future invocations""" +## """function decorator which calls function as classmethod, +## and replaces itself with result for current and all future invocations. +## """ ## def __init__(self, func): ## self.im_func = func ## @@ -238,480 +236,1254 @@ ## value = func(cls) ## setattr(cls, func.__name__, value) ## return value - -#works but not used... -##def abstractmethod(func): -## """Method decorator which indicates this is a placeholder method which -## should be overridden by subclass. -## -## If called directly, this method will raise an :exc:`NotImplementedError`. -## """ -## msg = "object %(self)r method %(name)r is abstract, and must be subclassed" -## def wrapper(self, *args, **kwds): -## text = msg % dict(self=self, name=wrapper.__name__) -## raise NotImplementedError(text) -## update_wrapper(wrapper, func) -## return wrapper - -#works but not used... -##def abstractclassmethod(func): -## """Class Method decorator which indicates this is a placeholder method which -## should be overridden by subclass, and must be a classmethod. ## -## If called directly, this method will raise an :exc:`NotImplementedError`. -## """ -## msg = "class %(cls)r method %(name)r is abstract, and must be subclassed" -## def wrapper(cls, *args, **kwds): -## text = msg % dict(cls=cls, name=wrapper.__name__) -## raise NotImplementedError(text) -## update_wrapper(wrapper, func) -## return classmethod(wrapper) - -#========================================================== -#protocol helpers -#========================================================== -def is_crypt_handler(obj): - "check if object follows the :ref:`password-hash-api`" - return all(hasattr(obj, name) for name in ( - "name", - "setting_kwds", "context_kwds", - "genconfig", "genhash", - "verify", "encrypt", "identify", - )) +## @property +## def __func__(self): +## "py3 compatible alias" + +#============================================================================= +# unicode helpers +#============================================================================= + +def consteq(left, right): + """Check two strings/bytes for equality. + This is functionally equivalent to ``left == right``, + but attempts to take constant time relative to the size of the righthand input. + + The purpose of this function is to help prevent timing attacks + during digest comparisons: the standard ``==`` operator aborts + after the first mismatched character, causing it's runtime to be + proportional to the longest prefix shared by the two inputs. + If an attacker is able to predict and control one of the two + inputs, repeated queries can be leveraged to reveal information about + the content of the second argument. To minimize this risk, :func:`!consteq` + is designed to take ``THETA(len(right))`` time, regardless + of the contents of the two strings. + It is recommended that the attacker-controlled input + be passed in as the left-hand value. + + .. warning:: + + This function is *not* perfect. Various VM-dependant issues + (e.g. the VM's integer object instantiation algorithm, internal unicode representation, etc), + may still cause the function's run time to be affected by the inputs, + though in a less predictable manner. + *To minimize such risks, this function should not be passed* :class:`unicode` + *inputs that might contain non-* ``ASCII`` *characters*. -def is_crypt_context(obj): - "check if object appears to be a :class:`~passlib.context.CryptContext` instance" - return all(hasattr(obj, name) for name in ( - "hash_needs_update", - "genconfig", "genhash", - "verify", "encrypt", "identify", - )) + .. versionadded:: 1.6 + """ + # NOTE: + # resources & discussions considered in the design of this function: + # hmac timing attack -- + # http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/ + # python developer discussion surrounding similar function -- + # http://bugs.python.org/issue15061 + # http://bugs.python.org/issue14955 + + # validate types + if isinstance(left, unicode): + if not isinstance(right, unicode): + raise TypeError("inputs must be both unicode or both bytes") + is_py3_bytes = False + elif isinstance(left, bytes): + if not isinstance(right, bytes): + raise TypeError("inputs must be both unicode or both bytes") + is_py3_bytes = PY3 + else: + raise TypeError("inputs must be both unicode or both bytes") -##def has_many_backends(handler): -## "check if handler provides multiple baceknds" -## #NOTE: should also provide get_backend(), .has_backend(), and .backends attr -## return hasattr(handler, "set_backend") + # do size comparison. + # NOTE: the double-if construction below is done deliberately, to ensure + # the same number of operations (including branches) is performed regardless + # of whether left & right are the same size. + same_size = (len(left) == len(right)) + if same_size: + # if sizes are the same, setup loop to perform actual check of contents. + tmp = left + result = 0 + if not same_size: + # if sizes aren't the same, set 'result' so equality will fail regardless + # of contents. then, to ensure we do exactly 'len(right)' iterations + # of the loop, just compare 'right' against itself. + tmp = right + result = 1 + + # run constant-time string comparision + if is_py3_bytes: + for l,r in zip(tmp, right): + result |= l ^ r + else: + for l,r in zip(tmp, right): + result |= ord(l) ^ ord(r) + return result == 0 -def has_rounds_info(handler): - "check if handler provides the optional :ref:`rounds information ` attributes" - return 'rounds' in handler.setting_kwds and getattr(handler, "min_rounds", None) is not None +def splitcomma(source, sep=","): + """split comma-separated string into list of elements, + stripping whitespace. + """ + source = source.strip() + if source.endswith(sep): + source = source[:-1] + if not source: + return [] + return [ elem.strip() for elem in source.split(sep) ] + +def saslprep(source, param="value"): + """Normalizes unicode string using SASLPrep stringprep profile. + + The SASLPrep profile is defined in :rfc:`4013`. + It provides a uniform scheme for normalizing unicode usernames + and passwords before performing byte-value sensitive operations + such as hashing. Among other things, it normalizes diacritic + representations, removes non-printing characters, and forbids + invalid characters such as ``\\n``. + + :arg source: + unicode string to normalize & validate + + :param param: + Optional noun used to refer to identify source parameter in error messages + (Defaults to the string ``"value"``). This is mainly useful to make the caller's error + messages make more sense. -def has_salt_info(handler): - "check if handler provides the optional :ref:`salt information ` attributes" - return 'salt' in handler.setting_kwds and getattr(handler, "min_salt_size", None) is not None + :raises ValueError: + if any characters forbidden by the SASLPrep profile are encountered. -##def has_raw_salt(handler): -## "check if handler takes in encoded salt as unicode (False), or decoded salt as bytes (True)" -## sc = getattr(handler, "salt_chars", None) -## if sc is None: -## return None -## elif isinstance(sc, unicode): -## return False -## elif isinstance(sc, bytes): -## return True -## else: -## raise TypeError("handler.salt_chars must be None/unicode/bytes") + :returns: + normalized unicode string + + .. note:: -#========================================================== -#bytes <-> unicode conversion helpers -#========================================================== - -def to_bytes(source, encoding="utf-8", source_encoding=None, errname="value"): - """helper to encoding unicode -> bytes - - this function takes in a ``source`` string. - if unicode, encodes it using the specified ``encoding``. - if bytes, returns unchanged - unless ``source_encoding`` - is specified, in which case the bytes are transcoded - if and only if the source encoding doesn't match - the desired encoding. - all other types result in a :exc:`TypeError`. - - :arg source: source bytes/unicode to process - :arg encoding: target character encoding or ``None``. - :param source_encoding: optional source encoding - :param errname: optional name of variable/noun to reference when raising errors + This function is not available under Jython, + as the Jython stdlib is missing the :mod:`!stringprep` module + (`Jython issue 1758320 `_). + """ + # saslprep - http://tools.ietf.org/html/rfc4013 + # stringprep - http://tools.ietf.org/html/rfc3454 + # http://docs.python.org/library/stringprep.html + + # validate type + if not isinstance(source, unicode): + raise TypeError("input must be unicode string, not %s" % + (type(source),)) + + # mapping stage + # - map non-ascii spaces to U+0020 (stringprep C.1.2) + # - strip 'commonly mapped to nothing' chars (stringprep B.1) + in_table_c12 = stringprep.in_table_c12 + in_table_b1 = stringprep.in_table_b1 + data = join_unicode( + _USPACE if in_table_c12(c) else c + for c in source + if not in_table_b1(c) + ) - :raises TypeError: if unicode encountered but ``encoding=None`` specified; - or if source is not unicode or bytes. + # normalize to KC form + data = unicodedata.normalize('NFKC', data) + if not data: + return _UEMPTY + + # check for invalid bi-directional strings. + # stringprep requires the following: + # - chars in C.8 must be prohibited. + # - if any R/AL chars in string: + # - no L chars allowed in string + # - first and last must be R/AL chars + # this checks if start/end are R/AL chars. if so, prohibited loop + # will forbid all L chars. if not, prohibited loop will forbid all + # R/AL chars instead. in both cases, prohibited loop takes care of C.8. + is_ral_char = stringprep.in_table_d1 + if is_ral_char(data[0]): + if not is_ral_char(data[-1]): + raise ValueError("malformed bidi sequence in " + param) + # forbid L chars within R/AL sequence. + is_forbidden_bidi_char = stringprep.in_table_d2 + else: + # forbid R/AL chars if start not setup correctly; L chars allowed. + is_forbidden_bidi_char = is_ral_char - :returns: bytes object + # check for prohibited output - stringprep tables A.1, B.1, C.1.2, C.2 - C.9 + in_table_a1 = stringprep.in_table_a1 + in_table_c21_c22 = stringprep.in_table_c21_c22 + in_table_c3 = stringprep.in_table_c3 + in_table_c4 = stringprep.in_table_c4 + in_table_c5 = stringprep.in_table_c5 + in_table_c6 = stringprep.in_table_c6 + in_table_c7 = stringprep.in_table_c7 + in_table_c8 = stringprep.in_table_c8 + in_table_c9 = stringprep.in_table_c9 + for c in data: + # check for this mapping stage should have removed + assert not in_table_b1(c), "failed to strip B.1 in mapping stage" + assert not in_table_c12(c), "failed to replace C.1.2 in mapping stage" + + # check for forbidden chars + if in_table_a1(c): + raise ValueError("unassigned code points forbidden in " + param) + if in_table_c21_c22(c): + raise ValueError("control characters forbidden in " + param) + if in_table_c3(c): + raise ValueError("private use characters forbidden in " + param) + if in_table_c4(c): + raise ValueError("non-char code points forbidden in " + param) + if in_table_c5(c): + raise ValueError("surrogate codes forbidden in " + param) + if in_table_c6(c): + raise ValueError("non-plaintext chars forbidden in " + param) + if in_table_c7(c): + # XXX: should these have been caught by normalize? + # if so, should change this to an assert + raise ValueError("non-canonical chars forbidden in " + param) + if in_table_c8(c): + raise ValueError("display-modifying / deprecated chars " + "forbidden in" + param) + if in_table_c9(c): + raise ValueError("tagged characters forbidden in " + param) + + # do bidi constraint check chosen by bidi init, above + if is_forbidden_bidi_char(c): + raise ValueError("forbidden bidi character in " + param) + + return data + +# replace saslprep() with stub when stringprep is missing +if stringprep is None: # pragma: no cover -- runtime detection + def saslprep(source, param="value"): + "stub for saslprep()" + raise NotImplementedError("saslprep() support requires the 'stringprep' " + "module, which is " + _stringprep_missing_reason) + +#============================================================================= +# bytes helpers +#============================================================================= +def render_bytes(source, *args): + """Peform ``%`` formating using bytes in a uniform manner across Python 2/3. - .. note:: + This function is motivated by the fact that + :class:`bytes` instances do not support ``%`` or ``{}`` formatting under Python 3. + This function is an attempt to provide a replacement: + it converts everything to unicode (decoding bytes instances as ``latin-1``), + performs the required formatting, then encodes the result to ``latin-1``. - if ``encoding`` is set to ``None``, then unicode strings - will be rejected, and only byte strings will be allowed through. + Calling ``render_bytes(source, *args)`` should function roughly the same as + ``source % args`` under Python 2. """ if isinstance(source, bytes): - if source_encoding and encoding and \ - not is_same_codec(source_encoding, encoding): + source = source.decode("latin-1") + result = source % tuple(arg.decode("latin-1") if isinstance(arg, bytes) + else arg for arg in args) + return result.encode("latin-1") + +if PY_MIN_32: + def bytes_to_int(value): + return int.from_bytes(value, 'big') + def int_to_bytes(value, count): + return value.to_bytes(count, 'big') +else: + # XXX: can any of these be sped up? + from binascii import hexlify, unhexlify + def bytes_to_int(value): + return int(hexlify(value),16) + if PY3: + # grr, why did py3 have to break % for bytes? + def int_to_bytes(value, count): + return unhexlify((('%%0%dx' % (count<<1)) % value).encode("ascii")) + else: + def int_to_bytes(value, count): + return unhexlify(('%%0%dx' % (count<<1)) % value) + +add_doc(bytes_to_int, "decode byte string as single big-endian integer") +add_doc(int_to_bytes, "encode integer as single big-endian byte string") + +def xor_bytes(left, right): + "Perform bitwise-xor of two byte strings (must be same size)" + return int_to_bytes(bytes_to_int(left) ^ bytes_to_int(right), len(left)) + +def repeat_string(source, size): + "repeat or truncate string, so it has length " + cur = len(source) + if size > cur: + mult = (size+cur-1)//cur + return (source*mult)[:size] + else: + return source[:size] + +_BNULL = b("\x00") +_UNULL = u("\x00") + +def right_pad_string(source, size, pad=None): + "right-pad or truncate string, so it has length " + cur = len(source) + if size > cur: + if pad is None: + pad = _UNULL if isinstance(source, unicode) else _BNULL + return source+pad*(size-cur) + else: + return source[:size] + +#============================================================================= +# encoding helpers +#============================================================================= +_ASCII_TEST_BYTES = b("\x00\n aA:#!\x7f") +_ASCII_TEST_UNICODE = _ASCII_TEST_BYTES.decode("ascii") + +def is_ascii_codec(codec): + "Test if codec is compatible with 7-bit ascii (e.g. latin-1, utf-8; but not utf-16)" + return _ASCII_TEST_UNICODE.encode(codec) == _ASCII_TEST_BYTES + +def is_same_codec(left, right): + "Check if two codec names are aliases for same codec" + if left == right: + return True + if not (left and right): + return False + return _lookup_codec(left).name == _lookup_codec(right).name + +_B80 = b('\x80')[0] +_U80 = u('\x80') +def is_ascii_safe(source): + "Check if string (bytes or unicode) contains only 7-bit ascii" + r = _B80 if isinstance(source, bytes) else _U80 + return all(c < r for c in source) + +def to_bytes(source, encoding="utf-8", param="value", source_encoding=None): + """Helper to normalize input to bytes. + + :arg source: + Source bytes/unicode to process. + + :arg encoding: + Target encoding (defaults to ``"utf-8"``). + + :param param: + Optional name of variable/noun to reference when raising errors + + :param source_encoding: + If this is specified, and the source is bytes, + the source will be transcoded from *source_encoding* to *encoding* + (via unicode). + + :raises TypeError: if source is not unicode or bytes. + + :returns: + * unicode strings will be encoded using *encoding*, and returned. + * if *source_encoding* is not specified, byte strings will be + returned unchanged. + * if *source_encoding* is specified, byte strings will be transcoded + to *encoding*. + """ + assert encoding + if isinstance(source, bytes): + if source_encoding and not is_same_codec(source_encoding, encoding): return source.decode(source_encoding).encode(encoding) else: return source - elif not encoding: - raise TypeError("%s must be bytes, not %s" % (errname, type(source))) elif isinstance(source, unicode): return source.encode(encoding) - elif source_encoding: - raise TypeError("%s must be unicode or %s-encoded bytes, not %s" % - (errname, source_encoding, type(source))) else: - raise TypeError("%s must be unicode or bytes, not %s" % (errname, type(source))) + raise ExpectedStringError(source, param) -def to_unicode(source, source_encoding="utf-8", errname="value"): - """take in unicode or bytes, return unicode +def to_unicode(source, encoding="utf-8", param="value"): + """Helper to normalize input to unicode. - if bytes provided, decodes using specified encoding. - leaves unicode alone. + :arg source: + source bytes/unicode to process. - :raises TypeError: if source is not unicode or bytes. + :arg encoding: + encoding to use when decoding bytes instances. - :arg source: source bytes/unicode to process - :arg source_encoding: encoding to use when decoding bytes instances - :param errname: optional name of variable/noun to reference when raising errors + :param param: + optional name of variable/noun to reference when raising errors. + + :raises TypeError: if source is not unicode or bytes. - :returns: unicode object + :returns: + * returns unicode strings unchanged. + * returns bytes strings decoded using *encoding* """ + assert encoding if isinstance(source, unicode): return source - elif not source_encoding: - raise TypeError("%s must be unicode, not %s" % (errname, type(source))) elif isinstance(source, bytes): - return source.decode(source_encoding) + return source.decode(encoding) else: - raise TypeError("%s must be unicode or %s-encoded bytes, not %s" % - (errname, source_encoding, type(source))) + raise ExpectedStringError(source, param) + +if PY3: + def to_native_str(source, encoding="utf-8", param="value"): + if isinstance(source, bytes): + return source.decode(encoding) + elif isinstance(source, unicode): + return source + else: + raise ExpectedStringError(source, param) +else: + def to_native_str(source, encoding="utf-8", param="value"): + if isinstance(source, bytes): + return source + elif isinstance(source, unicode): + return source.encode(encoding) + else: + raise ExpectedStringError(source, param) -def to_native_str(source, encoding="utf-8", errname="value"): - """take in unicode or bytes, return native string +add_doc(to_native_str, + """Take in unicode or bytes, return native string. - python 2: encodes unicode using specified encoding, leaves bytes alone. - python 3: decodes bytes using specified encoding, leaves unicode alone. + Python 2: encodes unicode using specified encoding, leaves bytes alone. + Python 3: leaves unicode alone, decodes bytes using specified encoding. :raises TypeError: if source is not unicode or bytes. - :arg source: source bytes/unicode to process - :arg encoding: encoding to use when encoding unicode / decoding bytes - :param errname: optional name of variable/noun to reference when raising errors + :arg source: + source unicode or bytes string. + + :arg encoding: + encoding to use when encoding unicode or decoding bytes. + this defaults to ``"utf-8"``. + + :param param: + optional name of variable/noun to reference when raising errors. :returns: :class:`str` instance - """ - assert encoding - if isinstance(source, bytes): - # Py2k # - return source - # Py3k # - #return source.decode(encoding) - # end Py3k # + """) - elif isinstance(source, unicode): - # Py2k # - return source.encode(encoding) - # Py3k # - #return source - # end Py3k # +@deprecated_function(deprecated="1.6", removed="1.7") +def to_hash_str(source, encoding="ascii"): # pragma: no cover -- deprecated & unused + "deprecated, use to_native_str() instead" + return to_native_str(source, encoding, param="hash") + +#============================================================================= +# base64-variant encoding +#============================================================================= + +class Base64Engine(object): + """Provides routines for encoding/decoding base64 data using + arbitrary character mappings, selectable endianness, etc. + + :arg charmap: + A string of 64 unique characters, + which will be used to encode successive 6-bit chunks of data. + A character's position within the string should correspond + to it's 6-bit value. - else: - raise TypeError("%s must be unicode or bytes, not %s" % (errname, type(source))) + :param big: + Whether the encoding should be big-endian (default False). -def to_hash_str(hash, encoding="ascii", errname="hash"): - "given hash string as bytes or unicode; normalize according to hash policy" - #NOTE: for now, policy is ascii-bytes under py2, unicode under py3. - # but plan to make flag allowing apps to enable unicode behavior under py2. - return to_native_str(hash, encoding, errname) - -#-------------------------------------------------- -#support utils -#-------------------------------------------------- -def is_same_codec(left, right): - "check if two codecs names are aliases for same codec" - if left == right: - return True - if not (left and right): - return False - return _lookup_codec(left).name == _lookup_codec(right).name + .. note:: + This class does not currently handle base64's padding characters + in any way what so ever. -_U80 = u'\x80' -_B80 = b('\x80') + Raw Bytes <-> Encoded Bytes + =========================== + The following methods convert between raw bytes, + and strings encoded using the engine's specific base64 variant: -def is_ascii_safe(source): - "check if source (bytes or unicode) contains only 7-bit ascii" - if isinstance(source, bytes): - # Py2k # - return all(c < _B80 for c in source) - # Py3k # - #return all(c < 128 for c in source) - # end Py3k # - else: - return all(c < _U80 for c in source) + .. automethod:: encode_bytes + .. automethod:: decode_bytes + .. automethod:: encode_transposed_bytes + .. automethod:: decode_transposed_bytes -#================================================================================= -#string helpers -#================================================================================= -def splitcomma(source, sep=","): - "split comma-separated string into list of elements, stripping whitespace and discarding empty elements" - return [ - elem.strip() - for elem in source.split(sep) - if elem.strip() - ] + .. + .. automethod:: check_repair_unused + .. automethod:: repair_unused -#========================================================== -#bytes helpers -#========================================================== - -#some common constants / aliases -BEMPTY = b('') - -#helpers for joining / extracting elements -bjoin = BEMPTY.join -ujoin = u''.join - -def belem_join(elems): - """takes series of bytes elements, returns bytes. - - elem should be result of bytes[x]. - this is another bytes instance under py2, - but it int under py3. - - returns bytes. - - this is bytes() constructor under py3, - but b"".join() under py2. - """ - # Py2k # - return bjoin(elems) - # Py3k # - #return bytes(elems) - # end Py3k # - -#for efficiency, don't bother with above wrapper... -# Py2k # -belem_join = bjoin -# Py3k # -#belem_join = bytes -# end Py3k # - -def bord(elem): - """takes bytes element, returns integer. - - elem should be result of bytes[x]. - this is another bytes instance under py2, - but it int under py3. - - returns int in range(0,256). - - this is ord() under py2, and noop under py3. - """ - # Py2k # - assert isinstance(elem, bytes) - return ord(elem) - # Py3k # - ##assert isinstance(elem, int) - #return elem - # end Py3k # - -#for efficiency, don't bother with wrapper -# Py2k # -bord = ord -# end Py2k # - -def bchrs(*values): - "takes series of ints, returns bytes; like chr() but for bytes, and w/ multi args" - # Py2k # - return bjoin(chr(v) for v in values) - # Py3k # - #return bytes(values) - # end Py3k # - -# Py2k # -def bjoin_ints(values): - return bjoin(chr(v) for v in values) -# Py3k # -#bjoin_ints = bytes -# end Py3k # + Integers <-> Encoded Bytes + ========================== + The following methods allow encoding and decoding + unsigned integers to and from the engine's specific base64 variant. + Endianess is determined by the engine's ``big`` constructor keyword. -def render_bytes(source, *args): - """helper for using formatting operator with bytes. + .. automethod:: encode_int6 + .. automethod:: decode_int6 + + .. automethod:: encode_int12 + .. automethod:: decode_int12 + + .. automethod:: encode_int24 + .. automethod:: decode_int24 - this function is motivated by the fact that - :class:`bytes` instances do not support % or {} formatting under python 3. - this function is an attempt to provide a replacement - that will work uniformly under python 2 & 3. + .. automethod:: encode_int64 + .. automethod:: decode_int64 - it converts everything to unicode (including bytes arguments), - then encodes the result to latin-1. + Informational Attributes + ======================== + .. attribute:: charmap + + unicode string containing list of characters used in encoding; + position in string matches 6bit value of character. + + .. attribute:: bytemap + + bytes version of :attr:`charmap` + + .. attribute:: big + + boolean flag indicating this using big-endian encoding. """ - if isinstance(source, bytes): - source = source.decode("latin-1") - def adapt(arg): - if isinstance(arg, bytes): - return arg.decode("latin-1") - return arg - result = source % tuple(adapt(arg) for arg in args) - return result.encode("latin-1") -#================================================================================= -#numeric helpers -#================================================================================= + #=================================================================== + # instance attrs + #=================================================================== + # public config + bytemap = None # charmap as bytes + big = None # little or big endian + + # filled in by init based on charmap. + # (byte elem: single byte under py2, 8bit int under py3) + _encode64 = None # maps 6bit value -> byte elem + _decode64 = None # maps byte elem -> 6bit value + + # helpers filled in by init based on endianness + _encode_bytes = None # throws IndexError if bad value (shouldn't happen) + _decode_bytes = None # throws KeyError if bad char. + + #=================================================================== + # init + #=================================================================== + def __init__(self, charmap, big=False): + # validate charmap, generate encode64/decode64 helper functions. + if isinstance(charmap, unicode): + charmap = charmap.encode("latin-1") + elif not isinstance(charmap, bytes): + raise ExpectedStringError(charmap, "charmap") + if len(charmap) != 64: + raise ValueError("charmap must be 64 characters in length") + if len(set(charmap)) != 64: + raise ValueError("charmap must not contain duplicate characters") + self.bytemap = charmap + self._encode64 = charmap.__getitem__ + lookup = dict((value, idx) for idx, value in enumerate(charmap)) + self._decode64 = lookup.__getitem__ + + # validate big, set appropriate helper functions. + self.big = big + if big: + self._encode_bytes = self._encode_bytes_big + self._decode_bytes = self._decode_bytes_big + else: + self._encode_bytes = self._encode_bytes_little + self._decode_bytes = self._decode_bytes_little -##def int_to_bytes(value, count=None, order="big"): -## """encode a integer into a string of bytes -## -## :arg value: the integer -## :arg count: optional number of bytes to expose, uses minimum needed if count not specified -## :param order: the byte ordering; "big" (the default), "little", or "native" -## -## :raises ValueError: -## * if count specified and integer too large to fit. -## * if integer is negative -## -## :returns: -## bytes encoding integer -## """ -## -## -##def bytes_to_int(value, order="big"): -## """decode a byte string into an integer representation of it's binary value. -## -## :arg value: the string to decode. -## :param order: the byte ordering; "big" (the default), "little", or "native" -## -## :returns: the decoded positive integer. -## """ -## if not value: -## return 0 -## if order == "native": -## order = sys.byteorder -## if order == "little": -## value = reversed(value) -## out = 0 -## for v in value: -## out = (out<<8) | ord(v) -## return out - -def bytes_to_int(value): - "decode string of bytes as single big-endian integer" - out = 0 - for v in value: - out = (out<<8) | bord(v) - return out - -def int_to_bytes(value, count): - "encodes integer into single big-endian byte string" - assert value < (1<<(8*count)), "value too large for %d bytes: %d" % (count, value) - return bjoin_ints( - ((value>>s) & 0xff) - for s in xrange(8*count-8,-8,-8) - ) + # TODO: support padding character + ##if padding is not None: + ## if isinstance(padding, unicode): + ## padding = padding.encode("latin-1") + ## elif not isinstance(padding, bytes): + ## raise TypeError("padding char must be unicode or bytes") + ## if len(padding) != 1: + ## raise ValueError("padding must be single character") + ##self.padding = padding + + @property + def charmap(self): + "charmap as unicode" + return self.bytemap.decode("latin-1") + + #=================================================================== + # encoding byte strings + #=================================================================== + def encode_bytes(self, source): + """encode bytes to base64 string. -def xor_bytes(left, right): - "perform bitwise-xor of two byte-strings" - #NOTE: this could use bjoin_ints(), but speed is *really* important here (c.f. PBKDF2) - # Py2k # - return bjoin(chr(ord(l) ^ ord(r)) for l, r in zip(left, right)) - # Py3k # - #return bytes(l ^ r for l, r in zip(left, right)) - # end Py3k # - -#================================================================================= -#alt base64 encoding -#================================================================================= + :arg source: byte string to encode. + :returns: byte string containing encoded data. + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + chunks, tail = divmod(len(source), 3) + if PY3: + next_value = iter(source).__next__ + else: + next_value = (ord(elem) for elem in source).next + gen = self._encode_bytes(next_value, chunks, tail) + out = join_byte_elems(imap(self._encode64, gen)) + ##if tail: + ## padding = self.padding + ## if padding: + ## out += padding * (3-tail) + return out + + def _encode_bytes_little(self, next_value, chunks, tail): + "helper used by encode_bytes() to handle little-endian encoding" + # + # output bit layout: + # + # first byte: v1 543210 + # + # second byte: v1 ....76 + # +v2 3210.. + # + # third byte: v2 ..7654 + # +v3 10.... + # + # fourth byte: v3 765432 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + yield v1 & 0x3f + yield ((v2 & 0x0f)<<2)|(v1>>6) + yield ((v3 & 0x03)<<4)|(v2>>4) + yield v3>>2 + idx += 1 + if tail: + v1 = next_value() + if tail == 1: + # note: 4 msb of last byte are padding + yield v1 & 0x3f + yield v1>>6 + else: + assert tail == 2 + # note: 2 msb of last byte are padding + v2 = next_value() + yield v1 & 0x3f + yield ((v2 & 0x0f)<<2)|(v1>>6) + yield v2>>4 + + def _encode_bytes_big(self, next_value, chunks, tail): + "helper used by encode_bytes() to handle big-endian encoding" + # + # output bit layout: + # + # first byte: v1 765432 + # + # second byte: v1 10.... + # +v2 ..7654 + # + # third byte: v2 3210.. + # +v3 ....76 + # + # fourth byte: v3 543210 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + yield v1>>2 + yield ((v1&0x03)<<4)|(v2>>4) + yield ((v2&0x0f)<<2)|(v3>>6) + yield v3 & 0x3f + idx += 1 + if tail: + v1 = next_value() + if tail == 1: + # note: 4 lsb of last byte are padding + yield v1>>2 + yield (v1&0x03)<<4 + else: + assert tail == 2 + # note: 2 lsb of last byte are padding + v2 = next_value() + yield v1>>2 + yield ((v1&0x03)<<4)|(v2>>4) + yield ((v2&0x0f)<<2) + + #=================================================================== + # decoding byte strings + #=================================================================== + + def decode_bytes(self, source): + """decode bytes from base64 string. + + :arg source: byte string to decode. + :returns: byte string containing decoded data. + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + ##padding = self.padding + ##if padding: + ## # TODO: add padding size check? + ## source = source.rstrip(padding) + chunks, tail = divmod(len(source), 4) + if tail == 1: + # only 6 bits left, can't encode a whole byte! + raise ValueError("input string length cannot be == 1 mod 4") + next_value = getattr(imap(self._decode64, source), next_method_attr) + try: + return join_byte_values(self._decode_bytes(next_value, chunks, tail)) + except KeyError: + err = exc_err() + raise ValueError("invalid character: %r" % (err.args[0],)) + + def _decode_bytes_little(self, next_value, chunks, tail): + "helper used by decode_bytes() to handle little-endian encoding" + # + # input bit layout: + # + # first byte: v1 ..543210 + # +v2 10...... + # + # second byte: v2 ....5432 + # +v3 3210.... + # + # third byte: v3 ......54 + # +v4 543210.. + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + v4 = next_value() + yield v1 | ((v2 & 0x3) << 6) + yield (v2>>2) | ((v3 & 0xF) << 4) + yield (v3>>4) | (v4<<2) + idx += 1 + if tail: + # tail is 2 or 3 + v1 = next_value() + v2 = next_value() + yield v1 | ((v2 & 0x3) << 6) + # NOTE: if tail == 2, 4 msb of v2 are ignored (should be 0) + if tail == 3: + # NOTE: 2 msb of v3 are ignored (should be 0) + v3 = next_value() + yield (v2>>2) | ((v3 & 0xF) << 4) + + def _decode_bytes_big(self, next_value, chunks, tail): + "helper used by decode_bytes() to handle big-endian encoding" + # + # input bit layout: + # + # first byte: v1 543210.. + # +v2 ......54 + # + # second byte: v2 3210.... + # +v3 ....5432 + # + # third byte: v3 10...... + # +v4 ..543210 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + v4 = next_value() + yield (v1<<2) | (v2>>4) + yield ((v2&0xF)<<4) | (v3>>2) + yield ((v3&0x3)<<6) | v4 + idx += 1 + if tail: + # tail is 2 or 3 + v1 = next_value() + v2 = next_value() + yield (v1<<2) | (v2>>4) + # NOTE: if tail == 2, 4 lsb of v2 are ignored (should be 0) + if tail == 3: + # NOTE: 2 lsb of v3 are ignored (should be 0) + v3 = next_value() + yield ((v2&0xF)<<4) | (v3>>2) + + #=================================================================== + # encode/decode helpers + #=================================================================== + + # padmap2/3 - dict mapping last char of string -> + # equivalent char with no padding bits set. + + def __make_padset(self, bits): + "helper to generate set of valid last chars & bytes" + pset = set(c for i,c in enumerate(self.bytemap) if not i & bits) + pset.update(c for i,c in enumerate(self.charmap) if not i & bits) + return frozenset(pset) + + @memoized_property + def _padinfo2(self): + "mask to clear padding bits, and valid last bytes (for strings 2 % 4)" + # 4 bits of last char unused (lsb for big, msb for little) + bits = 15 if self.big else (15<<2) + return ~bits, self.__make_padset(bits) + + @memoized_property + def _padinfo3(self): + "mask to clear padding bits, and valid last bytes (for strings 3 % 4)" + # 2 bits of last char unused (lsb for big, msb for little) + bits = 3 if self.big else (3<<4) + return ~bits, self.__make_padset(bits) + + def check_repair_unused(self, source): + """helper to detect & clear invalid unused bits in last character. + + :arg source: + encoded data (as ascii bytes or unicode). + + :returns: + `(True, result)` if the string was repaired, + `(False, source)` if the string was ok as-is. + """ + # figure out how many padding bits there are in last char. + tail = len(source) & 3 + if tail == 2: + mask, padset = self._padinfo2 + elif tail == 3: + mask, padset = self._padinfo3 + elif not tail: + return False, source + else: + raise ValueError("source length must != 1 mod 4") + + # check if last char is ok (padset contains bytes & unicode versions) + last = source[-1] + if last in padset: + return False, source + + # we have dirty bits - repair the string by decoding last char, + # clearing the padding bits via , and encoding new char. + if isinstance(source, unicode): + cm = self.charmap + last = cm[cm.index(last) & mask] + assert last in padset, "failed to generate valid padding char" + else: + # NOTE: this assumes ascii-compat encoding, and that + # all chars used by encoding are 7-bit ascii. + last = self._encode64(self._decode64(last) & mask) + assert last in padset, "failed to generate valid padding char" + if PY3: + last = bytes([last]) + return True, source[:-1] + last + + def repair_unused(self, source): + return self.check_repair_unused(source)[1] + + ##def transcode(self, source, other): + ## return ''.join( + ## other.charmap[self.charmap.index(char)] + ## for char in source + ## ) + + ##def random_encoded_bytes(self, size, random=None, unicode=False): + ## "return random encoded string of given size" + ## data = getrandstr(random or rng, + ## self.charmap if unicode else self.bytemap, size) + ## return self.repair_unused(data) + + #=================================================================== + # transposed encoding/decoding + #=================================================================== + def encode_transposed_bytes(self, source, offsets): + "encode byte string, first transposing source using offset list" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + tmp = join_byte_elems(source[off] for off in offsets) + return self.encode_bytes(tmp) + + def decode_transposed_bytes(self, source, offsets): + "decode byte string, then reverse transposition described by offset list" + # NOTE: if transposition does not use all bytes of source, + # the original can't be recovered... and join_byte_elems() will throw + # an error because 1+ values in will be None. + tmp = self.decode_bytes(source) + buf = [None] * len(offsets) + for off, char in zip(offsets, tmp): + buf[off] = char + return join_byte_elems(buf) + + #=================================================================== + # integer decoding helpers - mainly used by des_crypt family + #=================================================================== + def _decode_int(self, source, bits): + """decode base64 string -> integer + + :arg source: base64 string to decode. + :arg bits: number of bits in resulting integer. + + :raises ValueError: + * if the string contains invalid base64 characters. + * if the string is not long enough - it must be at least + ``int(ceil(bits/6))`` in length. + + :returns: + a integer in the range ``0 <= n < 2**bits`` + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + big = self.big + pad = -bits % 6 + chars = (bits+pad)/6 + if len(source) != chars: + raise ValueError("source must be %d chars" % (chars,)) + decode = self._decode64 + out = 0 + try: + for c in source if big else reversed(source): + out = (out<<6) + decode(c) + except KeyError: + raise ValueError("invalid character in string: %r" % (c,)) + if pad: + # strip padding bits + if big: + out >>= pad + else: + out &= (1< 6 bit integer" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 1: + raise ValueError("source must be exactly 1 byte") + if PY3: + # convert to 8bit int before doing lookup + source = source[0] + try: + return self._decode64(source) + except KeyError: + raise ValueError("invalid character") + + def decode_int12(self, source): + "decodes 2 char string -> 12-bit integer" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 2: + raise ValueError("source must be exactly 2 bytes") + decode = self._decode64 + try: + if self.big: + return decode(source[1]) + (decode(source[0])<<6) + else: + return decode(source[0]) + (decode(source[1])<<6) + except KeyError: + raise ValueError("invalid character") + + def decode_int24(self, source): + "decodes 4 char string -> 24-bit integer" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 4: + raise ValueError("source must be exactly 4 bytes") + decode = self._decode64 + try: + if self.big: + return decode(source[3]) + (decode(source[2])<<6)+ \ + (decode(source[1])<<12) + (decode(source[0])<<18) + else: + return decode(source[0]) + (decode(source[1])<<6)+ \ + (decode(source[2])<<12) + (decode(source[3])<<18) + except KeyError: + raise ValueError("invalid character") + + def decode_int64(self, source): + """decode 11 char base64 string -> 64-bit integer + + this format is used primarily by des-crypt & variants to encode + the DES output value used as a checksum. + """ + return self._decode_int(source, 64) + + #=================================================================== + # integer encoding helpers - mainly used by des_crypt family + #=================================================================== + def _encode_int(self, value, bits): + """encode integer into base64 format + + :arg value: non-negative integer to encode + :arg bits: number of bits to encode + + :returns: + a string of length ``int(ceil(bits/6.0))``. + """ + assert value >= 0, "caller did not sanitize input" + pad = -bits % 6 + bits += pad + if self.big: + itr = irange(bits-6, -6, -6) + # shift to add lsb padding. + value <<= pad + else: + itr = irange(0, bits, 6) + # padding is msb, so no change needed. + return join_byte_elems(imap(self._encode64, + ((value>>off) & 0x3f for off in itr))) + + #--------------------------------------------------------------- + # optimized versions for common integer sizes + #--------------------------------------------------------------- + + def encode_int6(self, value): + "encodes 6-bit integer -> single hash64 character" + if value < 0 or value > 63: + raise ValueError("value out of range") + if PY3: + return self.bytemap[value:value+1] + else: + return self._encode64(value) + + def encode_int12(self, value): + "encodes 12-bit integer -> 2 char string" + if value < 0 or value > 0xFFF: + raise ValueError("value out of range") + raw = [value & 0x3f, (value>>6) & 0x3f] + if self.big: + raw = reversed(raw) + return join_byte_elems(imap(self._encode64, raw)) + + def encode_int24(self, value): + "encodes 24-bit integer -> 4 char string" + if value < 0 or value > 0xFFFFFF: + raise ValueError("value out of range") + raw = [value & 0x3f, (value>>6) & 0x3f, + (value>>12) & 0x3f, (value>>18) & 0x3f] + if self.big: + raw = reversed(raw) + return join_byte_elems(imap(self._encode64, raw)) + + def encode_int64(self, value): + """encode 64-bit integer -> 11 char hash64 string + + this format is used primarily by des-crypt & variants to encode + the DES output value used as a checksum. + """ + if value < 0 or value > 0xffffffffffffffff: + raise ValueError("value out of range") + return self._encode_int(value, 64) + + #=================================================================== + # eof + #=================================================================== + +class LazyBase64Engine(Base64Engine): + "Base64Engine which delays initialization until it's accessed" + _lazy_opts = None + + def __init__(self, *args, **kwds): + self._lazy_opts = (args, kwds) + + def _lazy_init(self): + args, kwds = self._lazy_opts + super(LazyBase64Engine, self).__init__(*args, **kwds) + del self._lazy_opts + self.__class__ = Base64Engine + + def __getattribute__(self, attr): + if not attr.startswith("_"): + self._lazy_init() + return object.__getattribute__(self, attr) + +# common charmaps +BASE64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") +AB64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./") +HASH64_CHARS = u("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") +BCRYPT_CHARS = u("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + +# common variants +h64 = LazyBase64Engine(HASH64_CHARS) +h64big = LazyBase64Engine(HASH64_CHARS, big=True) +bcrypt64 = LazyBase64Engine(BCRYPT_CHARS, big=True) + +#============================================================================= +# adapted-base64 encoding +#============================================================================= _A64_ALTCHARS = b("./") _A64_STRIP = b("=\n") _A64_PAD1 = b("=") _A64_PAD2 = b("==") -def adapted_b64_encode(data): +def ab64_encode(data): """encode using variant of base64 - the output of this function is identical to b64_encode, + the output of this function is identical to stdlib's b64_encode, except that it uses ``.`` instead of ``+``, and omits trailing padding ``=`` and whitepsace. - it is primarily used for by passlib's custom pbkdf2 hashes. + it is primarily used by Passlib's custom pbkdf2 hashes. """ return b64encode(data, _A64_ALTCHARS).strip(_A64_STRIP) -def adapted_b64_decode(data, sixthree="."): +def ab64_decode(data): """decode using variant of base64 - the input of this function is identical to b64_decode, + the input of this function is identical to stdlib's b64_decode, except that it uses ``.`` instead of ``+``, and should not include trailing padding ``=`` or whitespace. - it is primarily used for by passlib's custom pbkdf2 hashes. + it is primarily used by Passlib's custom pbkdf2 hashes. """ - off = len(data) % 4 + off = len(data) & 3 if off == 0: return b64decode(data, _A64_ALTCHARS) - elif off == 1: - raise ValueError("invalid bas64 input") elif off == 2: return b64decode(data + _A64_PAD2, _A64_ALTCHARS) - else: + elif off == 3: return b64decode(data + _A64_PAD1, _A64_ALTCHARS) + else: # off == 1 + raise ValueError("invalid base64 input") + +#============================================================================= +# host OS helpers +#============================================================================= -#================================================================================= -#randomness -#================================================================================= +try: + from crypt import crypt as _crypt +except ImportError: # pragma: no cover + has_crypt = False + def safe_crypt(secret, hash): + return None +else: + has_crypt = True + _NULL = '\x00' -#----------------------------------------------------------------------- + # some crypt() variants will return various constant strings when + # an invalid/unrecognized config string is passed in; instead of + # returning NULL / None. examples include ":", ":0", "*0", etc. + # safe_crypt() returns None for any string starting with one of the + # chars in this string... + _invalid_prefixes = u("*:!") + + if PY3: + def safe_crypt(secret, hash): + if isinstance(secret, bytes): + # Python 3's crypt() only accepts unicode, which is then + # encoding using utf-8 before passing to the C-level crypt(). + # so we have to decode the secret. + orig = secret + try: + secret = secret.decode("utf-8") + except UnicodeDecodeError: + return None + assert secret.encode("utf-8") == orig, \ + "utf-8 spec says this can't happen!" + if _NULL in secret: + raise ValueError("null character in secret") + if isinstance(hash, bytes): + hash = hash.decode("ascii") + result = _crypt(secret, hash) + if not result or result[0] in _invalid_prefixes: + return None + return result + else: + def safe_crypt(secret, hash): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _NULL in secret: + raise ValueError("null character in secret") + if isinstance(hash, unicode): + hash = hash.encode("ascii") + result = _crypt(secret, hash) + if not result: + return None + result = result.decode("ascii") + if result[0] in _invalid_prefixes: + return None + return result + +add_doc(safe_crypt, """Wrapper around stdlib's crypt. + + This is a wrapper around stdlib's :func:`!crypt.crypt`, which attempts + to provide uniform behavior across Python 2 and 3. + + :arg secret: + password, as bytes or unicode (unicode will be encoded as ``utf-8``). + + :arg hash: + hash or config string, as ascii bytes or unicode. + + :returns: + resulting hash as ascii unicode; or ``None`` if the password + couldn't be hashed due to one of the issues: + + * :func:`crypt()` not available on platform. + + * Under Python 3, if *secret* is specified as bytes, + it must be use ``utf-8`` or it can't be passed + to :func:`crypt()`. + + * Some OSes will return ``None`` if they don't recognize + the algorithm being used (though most will simply fall + back to des-crypt). + + * Some OSes will return an error string if the input config + is recognized but malformed; current code converts these to ``None`` + as well. + """) + +def test_crypt(secret, hash): + """check if :func:`crypt.crypt` supports specific hash + :arg secret: password to test + :arg hash: known hash of password to use as reference + :returns: True or False + """ + assert secret and hash + return safe_crypt(secret, hash) == hash + +# pick best timer function to expose as "tick" - lifted from timeit module. +if sys.platform == "win32": + # On Windows, the best timer is time.clock() + from time import clock as tick +else: + # On most other platforms the best timer is time.time() + from time import time as tick + +#============================================================================= +# randomness +#============================================================================= + +#------------------------------------------------------------------------ # setup rng for generating salts -#----------------------------------------------------------------------- +#------------------------------------------------------------------------ -#NOTE: -# generating salts (eg h64_gensalt, below) doesn't require cryptographically +# NOTE: +# generating salts (e.g. h64_gensalt, below) doesn't require cryptographically # strong randomness. it just requires enough range of possible outputs -# that making a rainbow table is too costly. -# so python's builtin merseen twister prng is used, but seeded each time +# that making a rainbow table is too costly. so it should be ok to +# fall back on python's builtin mersenne twister prng, as long as it's seeded each time # this module is imported, using a couple of minor entropy sources. try: os.urandom(1) has_urandom = True -except NotImplementedError: #pragma: no cover +except NotImplementedError: # pragma: no cover has_urandom = False def genseed(value=None): "generate prng seed value from system resources" - #if value is rng, extract a bunch of bits from it's state + # if value is rng, extract a bunch of bits from it's state + from hashlib import sha512 if hasattr(value, "getrandbits"): - value = value.getrandbits(256) - text = u"%s %s %s %.15f %s" % ( + value = value.getrandbits(1<<15) + text = u("%s %s %s %.15f %.15f %s") % ( + # if caller specified a seed value (e.g. current rng state), mix it in value, - #if user specified a seed value (eg current rng state), mix it in + # add current process id + # NOTE: not available in some environments, e.g. GAE os.getpid() if hasattr(os, "getpid") else None, - #add current process id - #NOTE: not available in some environments, eg GAE + # id of a freshly created object. + # (at least 1 byte of which should be hard to predict) id(object()), - #id of a freshly created object. - #(at least 2 bytes of which should be hard to predict) + # the current time, to whatever precision os uses time.time(), - #the current time, to whatever precision os uses + time.clock(), - os.urandom(16).decode("latin-1") if has_urandom else 0, - #if urandom available, might as well mix some bytes in. + # if urandom available, might as well mix some bytes in. + os.urandom(32).decode("latin-1") if has_urandom else 0, ) - #hash it all up and return it as int - return long(sha256(text.encode("utf-8")).hexdigest(), 16) + # hash it all up and return it as int/long + return int(sha512(text.encode("utf-8")).hexdigest(), 16) if has_urandom: rng = random.SystemRandom() -else: #pragma: no cover - #NOTE: to reseed - rng.seed(genseed(rng)) +else: # pragma: no cover -- runtime detection + # NOTE: to reseed use ``rng.seed(genseed(rng))`` rng = random.Random(genseed()) -#----------------------------------------------------------------------- +#------------------------------------------------------------------------ # some rng helpers -#----------------------------------------------------------------------- - +#------------------------------------------------------------------------ def getrandbytes(rng, count): """return byte-string containing *count* number of randomly generated bytes, using specified rng""" - #NOTE: would be nice if this was present in stdlib Random class + # NOTE: would be nice if this was present in stdlib Random class ###just in case rng provides this... ##meth = getattr(rng, "getrandbytes", None) @@ -719,28 +1491,23 @@ ## return meth(count) if not count: - return BEMPTY + return _BEMPTY def helper(): - #XXX: break into chunks for large number of bits? + # XXX: break into chunks for large number of bits? value = rng.getrandbits(count<<3) i = 0 while i < count: - # Py2k # - yield chr(value & 0xff) - # Py3k # - #yield value & 0xff - # end Py3k # + yield value & 0xff value >>= 3 i += 1 - # Py2k # - return bjoin(helper()) - # Py3k # - #return bytes(helper()) - # end Py3k # + return join_byte_values(helper()) def getrandstr(rng, charset, count): """return string containing *count* number of chars/bytes, whose elements are drawn from specified charset, using specified rng""" - #check alphabet & count + # NOTE: tests determined this is 4x faster than rng.sample(), + # which is why that's not being used here. + + # check alphabet & count if count < 0: raise ValueError("count must be >= 0") letters = len(charset) @@ -749,9 +1516,9 @@ if letters == 1: return charset * count - #get random value, and write out to buffer + # get random value, and write out to buffer def helper(): - #XXX: break into chunks for large number of letters? + # XXX: break into chunks for large number of letters? value = rng.randrange(0, letters**count) i = 0 while i < count: @@ -760,16 +1527,14 @@ i += 1 if isinstance(charset, unicode): - return ujoin(helper()) + return join_unicode(helper()) else: - # Py2k # - return bjoin(helper()) - # Py3k # - #return bytes(helper()) - # end Py3k # + return join_byte_elems(helper()) -def generate_password(size=10, charset='2346789ABCDEFGHJKMNPQRTUVWXYZabcdefghjkmnpqrstuvwxyz'): - """generate random password using given length & chars +_52charset = '2346789ABCDEFGHJKMNPQRTUVWXYZabcdefghjkmnpqrstuvwxyz' + +def generate_password(size=10, charset=_52charset): + """generate random password using given length & charset :param size: size of password. @@ -781,10 +1546,68 @@ except for the characters ``1IiLl0OoS5``, which were omitted due to their visual similarity. - :returns: randomly generated password. + :returns: :class:`!str` containing randomly generated password. + + .. note:: + + Using the default character set, on a OS with :class:`!SystemRandom` support, + this function should generate passwords with 5.7 bits of entropy per character. """ return getrandstr(rng, charset, size) -#================================================================================= -#eof -#================================================================================= +#============================================================================= +# object type / interface tests +#============================================================================= +_handler_attrs = ( + "name", + "setting_kwds", "context_kwds", + "genconfig", "genhash", + "verify", "encrypt", "identify", + ) + +def is_crypt_handler(obj): + "check if object follows the :ref:`password-hash-api`" + # XXX: change to use isinstance(obj, PasswordHash) under py26+? + return all(hasattr(obj, name) for name in _handler_attrs) + +_context_attrs = ( + "needs_update", + "genconfig", "genhash", + "verify", "encrypt", "identify", + ) + +def is_crypt_context(obj): + "check if object appears to be a :class:`~passlib.context.CryptContext` instance" + # XXX: change to use isinstance(obj, CryptContext)? + return all(hasattr(obj, name) for name in _context_attrs) + +##def has_many_backends(handler): +## "check if handler provides multiple baceknds" +## # NOTE: should also provide get_backend(), .has_backend(), and .backends attr +## return hasattr(handler, "set_backend") + +def has_rounds_info(handler): + "check if handler provides the optional :ref:`rounds information ` attributes" + return ('rounds' in handler.setting_kwds and + getattr(handler, "min_rounds", None) is not None) + +def has_salt_info(handler): + "check if handler provides the optional :ref:`salt information ` attributes" + return ('salt' in handler.setting_kwds and + getattr(handler, "min_salt_size", None) is not None) + +##def has_raw_salt(handler): +## "check if handler takes in encoded salt as unicode (False), or decoded salt as bytes (True)" +## sc = getattr(handler, "salt_chars", None) +## if sc is None: +## return None +## elif isinstance(sc, unicode): +## return False +## elif isinstance(sc, bytes): +## return True +## else: +## raise TypeError("handler.salt_chars must be None/unicode/bytes") + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/_blowfish/__init__.py passlib-1.6.1/passlib/utils/_blowfish/__init__.py --- passlib-1.5.3/passlib/utils/_blowfish/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/utils/_blowfish/__init__.py 2012-08-01 17:14:10.000000000 +0000 @@ -0,0 +1,170 @@ +"""passlib.utils._blowfish - pure-python eks-blowfish implementation for bcrypt + +This is a pure-python implementation of the EKS-Blowfish algorithm described by +Provos and Mazieres in `A Future-Adaptable Password Scheme +`_. + +This package contains two submodules: + +* ``_blowfish/base.py`` contains a class implementing the eks-blowfish algorithm + using easy-to-examine code. + +* ``_blowfish/unrolled.py`` contains a subclass which replaces some methods + of the original class with sped-up versions, mainly using unrolled loops + and local variables. this is the class which is actually used by + Passlib to perform BCrypt in pure python. + + This module is auto-generated by a script, ``_blowfish/_gen_files.py``. + +Status +------ +This implementation is usuable, but is an order of magnitude too slow to be +usuable with real security. For "ok" security, BCrypt hashes should have at +least 2**11 rounds (as of 2011). Assuming a desired response time <= 100ms, +this means a BCrypt implementation should get at least 20 rounds/ms in order +to be both usuable *and* secure. On a 2 ghz cpu, this implementation gets +roughly 0.09 rounds/ms under CPython (220x too slow), and 1.9 rounds/ms +under PyPy (10x too slow). + +History +------- +While subsequently modified considerly for Passlib, this code was originally +based on `jBcrypt 0.2 `_, which was +released under the BSD license:: + + Copyright (c) 2006 Damien Miller + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +""" +#============================================================================= +# imports +#============================================================================= +# core +from itertools import chain +import struct +# pkg +from passlib.utils import bcrypt64, getrandbytes, rng +from passlib.utils.compat import b, bytes, BytesIO, unicode, u +from passlib.utils._blowfish.unrolled import BlowfishEngine +# local +__all__ = [ + 'BlowfishEngine', + 'raw_bcrypt', +] + +#============================================================================= +# bcrypt constants +#============================================================================= + +# bcrypt constant data "OrpheanBeholderScryDoubt" as 6 integers +BCRYPT_CDATA = [ + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 +] + +# struct used to encode ciphertext as digest (last output byte discarded) +digest_struct = struct.Struct(">6I") + +#============================================================================= +# base bcrypt helper +# +# interface designed only for use by passlib.handlers.bcrypt:BCrypt +# probably not suitable for other purposes +#============================================================================= +BNULL = b('\x00') + +def raw_bcrypt(password, ident, salt, log_rounds): + """perform central password hashing step in bcrypt scheme. + + :param password: the password to hash + :param ident: identifier w/ minor version (e.g. 2, 2a) + :param salt: the binary salt to use (encoded in bcrypt-base64) + :param rounds: the log2 of the number of rounds (as int) + :returns: bcrypt-base64 encoded checksum + """ + #=================================================================== + # parse inputs + #=================================================================== + + # parse ident + assert isinstance(ident, unicode) + if ident == u('2'): + minor = 0 + elif ident == u('2a'): + minor = 1 + # XXX: how to indicate caller wants to use crypt_blowfish's + # workaround variant of 2a? + elif ident == u('2x'): + raise ValueError("crypt_blowfish's buggy '2x' hashes are not " + "currently supported") + elif ident == u('2y'): + # crypt_blowfish compatibility ident which guarantees compat w/ 2a + minor = 1 + else: + raise ValueError("unknown ident: %r" % (ident,)) + + # decode & validate salt + assert isinstance(salt, bytes) + salt = bcrypt64.decode_bytes(salt) + if len(salt) < 16: + raise ValueError("Missing salt bytes") + elif len(salt) > 16: + salt = salt[:16] + + # prepare password + assert isinstance(password, bytes) + if minor > 0: + password += BNULL + + # validate rounds + if log_rounds < 4 or log_rounds > 31: + raise ValueError("Bad number of rounds") + + #=================================================================== + # + # run EKS-Blowfish algorithm + # + # This uses the "enhanced key schedule" step described by + # Provos and Mazieres in "A Future-Adaptable Password Scheme" + # http://www.openbsd.org/papers/bcrypt-paper.ps + # + #=================================================================== + + engine = BlowfishEngine() + + # convert password & salt into list of 18 32-bit integers. + pass_words = engine.key_to_words(password) + salt_words = engine.key_to_words(salt) + + # do EKS key schedule setup + # NOTE: [:4] is due to salt being 16 bytes originally, + # and the list needs to wrap properly + engine.eks_expand(pass_words, salt_words[:4]) + + # apply password & salt keys to key schedule a bunch more times. + rounds = 1<> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) +""".strip() + +def render_encipher(write, indent=0): + for i in irange(0, 15, 2): + write(indent, """\ + # Feistel substitution on left word (round %(i)d) + r ^= %(left)s ^ p%(i1)d + + # Feistel substitution on right word (round %(i1)d) + l ^= %(right)s ^ p%(i2)d + """, i=i, i1=i+1, i2=i+2, + left=BFSTR, right=BFSTR.replace("l","r"), + ) + +def write_encipher_function(write, indent=0): + write(indent, """\ + def encipher(self, l, r): + \"""blowfish encipher a single 64-bit block encoded as two 32-bit ints\""" + + (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) = self.P + S0, S1, S2, S3 = self.S + + l ^= p0 + + """) + render_encipher(write, indent+1) + + write(indent+1, """\ + + return r ^ p17, l + + """) + +def write_expand_function(write, indent=0): + write(indent, """\ + def expand(self, key_words): + \"""unrolled version of blowfish key expansion\""" + ##assert len(key_words) >= 18, "size of key_words must be >= 18" + + P, S = self.P, self.S + S0, S1, S2, S3 = S + + #============================================================= + # integrate key + #============================================================= + """) + for i in irange(18): + write(indent+1, """\ + p%(i)d = P[%(i)d] ^ key_words[%(i)d] + """, i=i) + write(indent+1, """\ + + #============================================================= + # update P + #============================================================= + + #------------------------------------------------ + # update P[0] and P[1] + #------------------------------------------------ + l, r = p0, 0 + + """) + + render_encipher(write, indent+1) + + write(indent+1, """\ + + p0, p1 = l, r = r ^ p17, l + + """) + + for i in irange(2, 18, 2): + write(indent+1, """\ + #------------------------------------------------ + # update P[%(i)d] and P[%(i1)d] + #------------------------------------------------ + l ^= p0 + + """, i=i, i1=i+1) + + render_encipher(write, indent+1) + + write(indent+1, """\ + p%(i)d, p%(i1)d = l, r = r ^ p17, l + + """, i=i, i1=i+1) + + write(indent+1, """\ + + #------------------------------------------------ + # save changes to original P array + #------------------------------------------------ + P[:] = (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) + + #============================================================= + # update S + #============================================================= + + for box in S: + j = 0 + while j < 256: + l ^= p0 + + """) + + render_encipher(write, indent+3) + + write(indent+3, """\ + + box[j], box[j+1] = l, r = r ^ p17, l + j += 2 + """) + +#============================================================================= +# main +#============================================================================= + +def main(): + target = os.path.join(os.path.dirname(__file__), "unrolled.py") + fh = file(target, "w") + + def write(indent, msg, **kwds): + literal = kwds.pop("literal", False) + if kwds: + msg %= kwds + if not literal: + msg = textwrap.dedent(msg.rstrip(" ")) + if indent: + msg = indent_block(msg, " " * (indent*4)) + fh.write(msg) + + write(0, """\ + \"""passlib.utils._blowfish.unrolled - unrolled loop implementation of bcrypt, + autogenerated by _gen_files.py + + currently this override the encipher() and expand() methods + with optimized versions, and leaves the other base.py methods alone. + \""" + #================================================================= + # imports + #================================================================= + # pkg + from passlib.utils._blowfish.base import BlowfishEngine as _BlowfishEngine + # local + __all__ = [ + "BlowfishEngine", + ] + #================================================================= + # + #================================================================= + class BlowfishEngine(_BlowfishEngine): + + """) + + write_encipher_function(write, indent=1) + write_expand_function(write, indent=1) + + write(0, """\ + #================================================================= + # eoc + #================================================================= + + #================================================================= + # eof + #================================================================= + """) + +if __name__ == "__main__": + main() + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/_blowfish/base.py passlib-1.6.1/passlib/utils/_blowfish/base.py --- passlib-1.5.3/passlib/utils/_blowfish/base.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/utils/_blowfish/base.py 2012-08-01 17:14:10.000000000 +0000 @@ -0,0 +1,442 @@ +"""passlib.utils._blowfish.base - unoptimized pure-python blowfish engine""" +#============================================================================= +# imports +#============================================================================= +# core +import struct +# pkg +from passlib.utils.compat import bytes +from passlib.utils import repeat_string +# local +__all__ = [ + "BlowfishEngine", +] + +#============================================================================= +# blowfish constants +#============================================================================= +BLOWFISH_P = BLOWFISH_S = None + +def _init_constants(): + global BLOWFISH_P, BLOWFISH_S + + # NOTE: blowfish's spec states these numbers are the hex representation + # of the fractional portion of PI, in order. + + # Initial contents of key schedule - 18 integers + BLOWFISH_P = [ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b, + ] + + # all 4 blowfish S boxes in one array - 256 integers per S box + BLOWFISH_S = [ + # sbox 1 + [ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + ], + # sbox 2 + [ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + ], + # sbox 3 + [ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + ], + # sbox 4 + [ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6, + ] + ] + +#============================================================================= +# engine +#============================================================================= +class BlowfishEngine(object): + + def __init__(self): + if BLOWFISH_P is None: + _init_constants() + self.P = list(BLOWFISH_P) + self.S = [ list(box) for box in BLOWFISH_S ] + + #=================================================================== + # common helpers + #=================================================================== + @staticmethod + def key_to_words(data, size=18): + """convert data to tuple of 4-byte integers, repeating or + truncating data as needed to reach specified size""" + assert isinstance(data, bytes) + dlen = len(data) + if not dlen: + # return all zeros - original C code would just read the NUL after + # the password, so mimicing that behavior for this edge case. + return [0]*size + + # repeat data until it fills up 4*size bytes + data = repeat_string(data, size<<2) + + # unpack + return struct.unpack(">%dI" % (size,), data) + + #=================================================================== + # blowfish routines + #=================================================================== + def encipher(self, l, r): + "loop version of blowfish encipher routine" + P, S = self.P, self.S + l ^= P[0] + i = 1 + while i < 17: + # Feistel substitution on left word + r = ((((S[0][l >> 24] + S[1][(l >> 16) & 0xff]) ^ S[2][(l >> 8) & 0xff]) + + S[3][l & 0xff]) & 0xffffffff) ^ P[i] ^ r + # swap vars so even rounds do Feistel substition on right word + l, r = r, l + i += 1 + return r ^ P[17], l + + # NOTE: decipher is same as above, just with reversed(P) instead. + + def expand(self, key_words): + "perform stock Blowfish keyschedule setup" + assert len(key_words) >= 18, "key_words must be at least as large as P" + P, S, encipher = self.P, self.S, self.encipher + + i = 0 + while i < 18: + P[i] ^= key_words[i] + i += 1 + + i = l = r = 0 + while i < 18: + P[i], P[i+1] = l,r = encipher(l,r) + i += 2 + + for box in S: + i = 0 + while i < 256: + box[i], box[i+1] = l,r = encipher(l,r) + i += 2 + + #=================================================================== + # eks-blowfish routines + #=================================================================== + def eks_expand(self, key_words, salt_words): + "perform EKS version of Blowfish keyschedule setup" + # NOTE: this is the same as expand(), except for the addition + # of the operations involving *salt_words*. + + assert len(key_words) >= 18, "key_words must be at least as large as P" + salt_size = len(salt_words) + assert salt_size, "salt_words must not be empty" + assert not salt_size & 1, "salt_words must have even length" + P, S, encipher = self.P, self.S, self.encipher + + i = 0 + while i < 18: + P[i] ^= key_words[i] + i += 1 + + s = i = l = r = 0 + while i < 18: + l ^= salt_words[s] + r ^= salt_words[s+1] + s += 2 + if s == salt_size: + s = 0 + P[i], P[i+1] = l,r = encipher(l,r) # next() + i += 2 + + for box in S: + i = 0 + while i < 256: + l ^= salt_words[s] + r ^= salt_words[s+1] + s += 2 + if s == salt_size: + s = 0 + box[i], box[i+1] = l,r = encipher(l,r) # next() + i += 2 + + def eks_rounds_expand0(self, key_words, salt_words, rounds): + "perform rounds stage of EKS keyschedule setup" + expand = self.expand + n = 0 + while n < rounds: + expand(key_words) + expand(salt_words) + n += 1 + + def repeat_encipher(self, l, r, count): + "repeatedly apply encipher operation to a block" + encipher = self.encipher + n = 0 + while n < count: + l, r = encipher(l, r) + n += 1 + return l, r + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/_blowfish/unrolled.py passlib-1.6.1/passlib/utils/_blowfish/unrolled.py --- passlib-1.5.3/passlib/utils/_blowfish/unrolled.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/utils/_blowfish/unrolled.py 2012-08-01 17:14:10.000000000 +0000 @@ -0,0 +1,771 @@ +"""passlib.utils._blowfish.unrolled - unrolled loop implementation of bcrypt, +autogenerated by _gen_files.py + +currently this override the encipher() and expand() methods +with optimized versions, and leaves the other base.py methods alone. +""" +#============================================================================= +# imports +#============================================================================= +# pkg +from passlib.utils._blowfish.base import BlowfishEngine as _BlowfishEngine +# local +__all__ = [ + "BlowfishEngine", +] +#============================================================================= +# +#============================================================================= +class BlowfishEngine(_BlowfishEngine): + + def encipher(self, l, r): + """blowfish encipher a single 64-bit block encoded as two 32-bit ints""" + + (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) = self.P + S0, S1, S2, S3 = self.S + + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + + return r ^ p17, l + + def expand(self, key_words): + """unrolled version of blowfish key expansion""" + ##assert len(key_words) >= 18, "size of key_words must be >= 18" + + P, S = self.P, self.S + S0, S1, S2, S3 = S + + #============================================================= + # integrate key + #============================================================= + p0 = P[0] ^ key_words[0] + p1 = P[1] ^ key_words[1] + p2 = P[2] ^ key_words[2] + p3 = P[3] ^ key_words[3] + p4 = P[4] ^ key_words[4] + p5 = P[5] ^ key_words[5] + p6 = P[6] ^ key_words[6] + p7 = P[7] ^ key_words[7] + p8 = P[8] ^ key_words[8] + p9 = P[9] ^ key_words[9] + p10 = P[10] ^ key_words[10] + p11 = P[11] ^ key_words[11] + p12 = P[12] ^ key_words[12] + p13 = P[13] ^ key_words[13] + p14 = P[14] ^ key_words[14] + p15 = P[15] ^ key_words[15] + p16 = P[16] ^ key_words[16] + p17 = P[17] ^ key_words[17] + + #============================================================= + # update P + #============================================================= + + #------------------------------------------------ + # update P[0] and P[1] + #------------------------------------------------ + l, r = p0, 0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + + p0, p1 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[2] and P[3] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p2, p3 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[4] and P[5] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p4, p5 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[6] and P[7] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p6, p7 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[8] and P[9] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p8, p9 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[10] and P[11] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p10, p11 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[12] and P[13] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p12, p13 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[14] and P[15] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p14, p15 = l, r = r ^ p17, l + + #------------------------------------------------ + # update P[16] and P[17] + #------------------------------------------------ + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + p16, p17 = l, r = r ^ p17, l + + + #------------------------------------------------ + # save changes to original P array + #------------------------------------------------ + P[:] = (p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, + p10, p11, p12, p13, p14, p15, p16, p17) + + #============================================================= + # update S + #============================================================= + + for box in S: + j = 0 + while j < 256: + l ^= p0 + + # Feistel substitution on left word (round 0) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p1 + + # Feistel substitution on right word (round 1) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p2 + # Feistel substitution on left word (round 2) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p3 + + # Feistel substitution on right word (round 3) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p4 + # Feistel substitution on left word (round 4) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p5 + + # Feistel substitution on right word (round 5) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p6 + # Feistel substitution on left word (round 6) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p7 + + # Feistel substitution on right word (round 7) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p8 + # Feistel substitution on left word (round 8) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p9 + + # Feistel substitution on right word (round 9) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p10 + # Feistel substitution on left word (round 10) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p11 + + # Feistel substitution on right word (round 11) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p12 + # Feistel substitution on left word (round 12) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p13 + + # Feistel substitution on right word (round 13) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p14 + # Feistel substitution on left word (round 14) + r ^= ((((S0[l >> 24] + S1[(l >> 16) & 0xff]) ^ S2[(l >> 8) & 0xff]) + + S3[l & 0xff]) & 0xffffffff) ^ p15 + + # Feistel substitution on right word (round 15) + l ^= ((((S0[r >> 24] + S1[(r >> 16) & 0xff]) ^ S2[(r >> 8) & 0xff]) + + S3[r & 0xff]) & 0xffffffff) ^ p16 + + box[j], box[j+1] = l, r = r ^ p17, l + j += 2 + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/compat.py passlib-1.6.1/passlib/utils/compat.py --- passlib-1.5.3/passlib/utils/compat.py 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/passlib/utils/compat.py 2012-08-01 17:14:10.000000000 +0000 @@ -0,0 +1,436 @@ +"""passlib.utils.compat - python 2/3 compatibility helpers""" +#============================================================================= +# figure out what we're running +#============================================================================= + +#------------------------------------------------------------------------ +# python version +#------------------------------------------------------------------------ +import sys +PY2 = sys.version_info < (3,0) +PY3 = sys.version_info >= (3,0) +PY_MAX_25 = sys.version_info < (2,6) # py 2.5 or earlier +PY27 = sys.version_info[:2] == (2,7) # supports last 2.x release +PY_MIN_32 = sys.version_info >= (3,2) # py 3.2 or later + +#------------------------------------------------------------------------ +# python implementation +#------------------------------------------------------------------------ +PYPY = hasattr(sys, "pypy_version_info") +JYTHON = sys.platform.startswith('java') + +#------------------------------------------------------------------------ +# capabilities +#------------------------------------------------------------------------ + +# __dir__() added in py2.6 +SUPPORTS_DIR_METHOD = not PY_MAX_25 and not (PYPY and sys.pypy_version_info < (1,6)) + +#============================================================================= +# common imports +#============================================================================= +import logging; log = logging.getLogger(__name__) +if PY3: + import builtins +else: + import __builtin__ as builtins + +def add_doc(obj, doc): + """add docstring to an object""" + obj.__doc__ = doc + +#============================================================================= +# the default exported vars +#============================================================================= +__all__ = [ + # python versions + 'PY2', 'PY3', 'PY_MAX_25', 'PY27', 'PY_MIN_32', + + # io + 'BytesIO', 'StringIO', 'NativeStringIO', 'SafeConfigParser', + 'print_', + + # type detection +## 'is_mapping', + 'callable', + 'int_types', + 'num_types', + 'base_string_types', + + # unicode/bytes types & helpers + 'u', 'b', + 'unicode', 'bytes', + 'uascii_to_str', 'bascii_to_str', + 'str_to_uascii', 'str_to_bascii', + 'join_unicode', 'join_bytes', + 'join_byte_values', 'join_byte_elems', + 'byte_elem_value', + 'iter_byte_values', + + # iteration helpers + 'irange', #'lrange', + 'imap', 'lmap', + 'iteritems', 'itervalues', + 'next', + + # introspection + 'exc_err', 'get_method_function', 'add_doc', +] + +# begin accumulating mapping of lazy-loaded attrs, +# 'merged' into module at bottom +_lazy_attrs = dict() + +#============================================================================= +# unicode & bytes types +#============================================================================= +if PY3: + unicode = str + bytes = builtins.bytes + + def u(s): + assert isinstance(s, str) + return s + + def b(s): + assert isinstance(s, str) + return s.encode("latin-1") + + base_string_types = (unicode, bytes) + +else: + unicode = builtins.unicode + bytes = str if PY_MAX_25 else builtins.bytes + + def u(s): + assert isinstance(s, str) + return s.decode("unicode_escape") + + def b(s): + assert isinstance(s, str) + return s + + base_string_types = basestring + +#============================================================================= +# unicode & bytes helpers +#============================================================================= +# function to join list of unicode strings +join_unicode = u('').join + +# function to join list of byte strings +join_bytes = b('').join + +if PY3: + def uascii_to_str(s): + assert isinstance(s, unicode) + return s + + def bascii_to_str(s): + assert isinstance(s, bytes) + return s.decode("ascii") + + def str_to_uascii(s): + assert isinstance(s, str) + return s + + def str_to_bascii(s): + assert isinstance(s, str) + return s.encode("ascii") + + join_byte_values = join_byte_elems = bytes + + def byte_elem_value(elem): + assert isinstance(elem, int) + return elem + + def iter_byte_values(s): + assert isinstance(s, bytes) + return s + + def iter_byte_chars(s): + assert isinstance(s, bytes) + # FIXME: there has to be a better way to do this + return (bytes([c]) for c in s) + +else: + def uascii_to_str(s): + assert isinstance(s, unicode) + return s.encode("ascii") + + def bascii_to_str(s): + assert isinstance(s, bytes) + return s + + def str_to_uascii(s): + assert isinstance(s, str) + return s.decode("ascii") + + def str_to_bascii(s): + assert isinstance(s, str) + return s + + def join_byte_values(values): + return join_bytes(chr(v) for v in values) + + join_byte_elems = join_bytes + + byte_elem_value = ord + + def iter_byte_values(s): + assert isinstance(s, bytes) + return (ord(c) for c in s) + + def iter_byte_chars(s): + assert isinstance(s, bytes) + return s + +add_doc(uascii_to_str, "helper to convert ascii unicode -> native str") +add_doc(bascii_to_str, "helper to convert ascii bytes -> native str") +add_doc(str_to_uascii, "helper to convert ascii native str -> unicode") +add_doc(str_to_bascii, "helper to convert ascii native str -> bytes") + +# join_byte_values -- function to convert list of ordinal integers to byte string. + +# join_byte_elems -- function to convert list of byte elements to byte string; +# i.e. what's returned by ``b('a')[0]``... +# this is b('a') under PY2, but 97 under PY3. + +# byte_elem_value -- function to convert byte element to integer -- a noop under PY3 + +add_doc(iter_byte_values, "iterate over byte string as sequence of ints 0-255") +add_doc(iter_byte_chars, "iterate over byte string as sequence of 1-byte strings") + +#============================================================================= +# numeric +#============================================================================= +if PY3: + int_types = (int,) + num_types = (int, float) +else: + int_types = (int, long) + num_types = (int, long, float) + +#============================================================================= +# iteration helpers +# +# irange - range iterable / view (xrange under py2, range under py3) +# lrange - range list (range under py2, list(range()) under py3) +# +# imap - map to iterator +# lmap - map to list +#============================================================================= +if PY3: + irange = range + ##def lrange(*a,**k): + ## return list(range(*a,**k)) + + def lmap(*a, **k): + return list(map(*a,**k)) + imap = map + + def iteritems(d): + return d.items() + def itervalues(d): + return d.values() + + next_method_attr = "__next__" + +else: + irange = xrange + ##lrange = range + + lmap = map + from itertools import imap + + def iteritems(d): + return d.iteritems() + def itervalues(d): + return d.itervalues() + + next_method_attr = "next" + +if PY_MAX_25: + _undef = object() + def next(itr, default=_undef): + "compat wrapper for next()" + if default is _undef: + return itr.next() + try: + return itr.next() + except StopIteration: + return default +else: + next = builtins.next + +#============================================================================= +# typing +#============================================================================= +##def is_mapping(obj): +## # non-exhaustive check, enough to distinguish from lists, etc +## return hasattr(obj, "items") + +if (3,0) <= sys.version_info < (3,2): + # callable isn't dead, it's just resting + from collections import Callable + def callable(obj): + return isinstance(obj, Callable) +else: + callable = builtins.callable + +#============================================================================= +# introspection +#============================================================================= +def exc_err(): + "return current error object (to avoid try/except syntax change)" + return sys.exc_info()[1] + +if PY3: + method_function_attr = "__func__" +else: + method_function_attr = "im_func" + +def get_method_function(func): + "given (potential) method, return underlying function" + return getattr(func, method_function_attr, func) + +#============================================================================= +# input/output +#============================================================================= +if PY3: + _lazy_attrs = dict( + BytesIO="io.BytesIO", + UnicodeIO="io.StringIO", + NativeStringIO="io.StringIO", + SafeConfigParser="configparser.SafeConfigParser", + ) + if sys.version_info >= (3,2): + # py32 renamed this, removing old ConfigParser + _lazy_attrs["SafeConfigParser"] = "configparser.ConfigParser" + + print_ = getattr(builtins, "print") + +else: + _lazy_attrs = dict( + BytesIO="cStringIO.StringIO", + UnicodeIO="StringIO.StringIO", + NativeStringIO="cStringIO.StringIO", + SafeConfigParser="ConfigParser.SafeConfigParser", + ) + + def print_(*args, **kwds): + """The new-style print function.""" + # extract kwd args + fp = kwds.pop("file", sys.stdout) + sep = kwds.pop("sep", None) + end = kwds.pop("end", None) + if kwds: + raise TypeError("invalid keyword arguments") + + # short-circuit if no target + if fp is None: + return + + # use unicode or bytes ? + want_unicode = isinstance(sep, unicode) or isinstance(end, unicode) or \ + any(isinstance(arg, unicode) for arg in args) + + # pick default end sequence + if end is None: + end = u("\n") if want_unicode else "\n" + elif not isinstance(end, base_string_types): + raise TypeError("end must be None or a string") + + # pick default separator + if sep is None: + sep = u(" ") if want_unicode else " " + elif not isinstance(sep, base_string_types): + raise TypeError("sep must be None or a string") + + # write to buffer + first = True + write = fp.write + for arg in args: + if first: + first = False + else: + write(sep) + if not isinstance(arg, basestring): + arg = str(arg) + write(arg) + write(end) + +#============================================================================= +# lazy overlay module +#============================================================================= +from types import ModuleType + +def _import_object(source): + "helper to import object from module; accept format `path.to.object`" + modname, modattr = source.rsplit(".",1) + mod = __import__(modname, fromlist=[modattr], level=0) + return getattr(mod, modattr) + +class _LazyOverlayModule(ModuleType): + """proxy module which overlays original module, + and lazily imports specified attributes. + + this is mainly used to prevent importing of resources + that are only needed by certain password hashes, + yet allow them to be imported from a single location. + + used by :mod:`passlib.utils`, :mod:`passlib.utils.crypto`, + and :mod:`passlib.utils.compat`. + """ + + @classmethod + def replace_module(cls, name, attrmap): + orig = sys.modules[name] + self = cls(name, attrmap, orig) + sys.modules[name] = self + return self + + def __init__(self, name, attrmap, proxy=None): + ModuleType.__init__(self, name) + self.__attrmap = attrmap + self.__proxy = proxy + self.__log = logging.getLogger(name) + + def __getattr__(self, attr): + proxy = self.__proxy + if proxy and hasattr(proxy, attr): + return getattr(proxy, attr) + attrmap = self.__attrmap + if attr in attrmap: + source = attrmap[attr] + if callable(source): + value = source() + else: + value = _import_object(source) + setattr(self, attr, value) + self.__log.debug("loaded lazy attr %r: %r", attr, value) + return value + raise AttributeError("'module' object has no attribute '%s'" % (attr,)) + + def __repr__(self): + proxy = self.__proxy + if proxy: + return repr(proxy) + else: + return ModuleType.__repr__(self) + + def __dir__(self): + attrs = set(dir(self.__class__)) + attrs.update(self.__dict__) + attrs.update(self.__attrmap) + proxy = self.__proxy + if proxy is not None: + attrs.update(dir(proxy)) + return list(attrs) + +# replace this module with overlay that will lazily import attributes. +_LazyOverlayModule.replace_module(__name__, _lazy_attrs) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/des.py passlib-1.6.1/passlib/utils/des.py --- passlib-1.5.3/passlib/utils/des.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/utils/des.py 2012-08-01 19:31:17.000000000 +0000 @@ -1,4 +1,5 @@ -""" +"""passlib.utils.des -- DES block encryption routines + History ======= These routines (which have since been drastically modified for python) @@ -32,56 +33,62 @@ @version $Id: UnixCrypt2.txt,v 1.1.1.1 2005/09/13 22:20:13 christos Exp $ @author Greg Wilkins (gregw) -netbsd des-crypt implementation, -which has some nice notes on how this all works - +The netbsd des-crypt implementation has some nice notes on how this all works - http://fxr.googlebit.com/source/lib/libcrypt/crypt.c?v=NETBSD-CURRENT """ -#TODO: could use an accelerated C version of this module to speed up lmhash, des-crypt, and ext-des-crypt +# TODO: could use an accelerated C version of this module to speed up lmhash, +# des-crypt, and ext-des-crypt -#========================================================= -#imports -#========================================================= -#pkg -from passlib.utils import bytes_to_int, int_to_bytes, bytes, bord, bjoin_ints -#local +#============================================================================= +# imports +#============================================================================= +# core +import struct +# pkg +from passlib import exc +from passlib.utils.compat import bytes, join_byte_values, byte_elem_value, \ + b, irange, irange, int_types +from passlib.utils import deprecated_function +# local __all__ = [ "expand_des_key", "des_encrypt_block", "mdes_encrypt_int_block", ] -#========================================================= -#precalculated iteration ranges & constants -#========================================================= -R8 = range(8) -RR8 = range(7, -1, -1) -RR4 = range(3, -1, -1) -RR12_1 = range(11, 1, -1) -RR9_1 = range(9,-1,-1) - -RR6_S2 = range(6, -1, -2) -RR14_S2 = range(14, -1, -2) -R16_S2 = range(0, 16, 2) - -INT_24_MAX = 0xffffff -INT_64_MAX = 0xffffffff -INT_64_MAX = 0xffffffffffffffff - -#========================================================= -# static tables for des -#========================================================= -PCXROT = IE3264 = SPE = CF6464 = None #placeholders filled in by load_tables +#============================================================================= +# constants +#============================================================================= + +# masks/upper limits for various integer sizes +INT_24_MASK = 0xffffff +INT_56_MASK = 0xffffffffffffff +INT_64_MASK = 0xffffffffffffffff + +# mask to clear parity bits from 64-bit key +_KDATA_MASK = 0xfefefefefefefefe +_KPARITY_MASK = 0x0101010101010101 + +# mask used to setup key schedule +_KS_MASK = 0xfcfcfcfcffffffff + +#============================================================================= +# static DES tables +#============================================================================= + +# placeholders filled in by _load_tables() +PCXROT = IE3264 = SPE = CF6464 = None -def load_tables(): +def _load_tables(): "delay loading tables until they are actually needed" global PCXROT, IE3264, SPE, CF6464 - #--------------------------------------------------- + #--------------------------------------------------------------- # Initial key schedule permutation # PC1ROT - bit reverse, then PC1, then Rotate, then PC2 - #--------------------------------------------------- - #NOTE: this was reordered from original table to make perm3264 logic simpler + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm3264 logic simpler PC1ROT=( ( 0x0000000000000000, 0x0000000000000000, 0x0000000000002000, 0x0000000000002000, 0x0000000000000020, 0x0000000000000020, 0x0000000000002020, 0x0000000000002020, @@ -148,11 +155,11 @@ 0x0000000800000000, 0x0000000880000000, 0x0000040800000000, 0x0000040880000000, 0x0010000800000000, 0x0010000880000000, 0x0010040800000000, 0x0010040880000000, ), ) - #--------------------------------------------------- + #--------------------------------------------------------------- # Subsequent key schedule rotation permutations # PC2ROT - PC2 inverse, then Rotate, then PC2 - #--------------------------------------------------- - #NOTE: this was reordered from original table to make perm3264 logic simpler + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm3264 logic simpler PC2ROTA=( ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000200000, 0x0000000000200000, 0x0000000000200000, 0x0000000000200000, @@ -220,7 +227,7 @@ 0x0008200000000000, 0x0088200000000000, 0x0008240000000000, 0x0088240000000000, ), ) - #NOTE: this was reordered from original table to make perm3264 logic simpler + # NOTE: this was reordered from original table to make perm3264 logic simpler PC2ROTB=( ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000400, 0x0000000000000400, 0x0000000000000400, 0x0000000000000400, @@ -287,10 +294,10 @@ 0x0004000000000000, 0x0104000000000000, 0x0005000000000000, 0x0105000000000000, 0x0004001000000000, 0x0104001000000000, 0x0005001000000000, 0x0105001000000000, ), ) - #--------------------------------------------------- - #PCXROT - PC1ROT, PC2ROTA, PC2ROTB listed in order + #--------------------------------------------------------------- + # PCXROT - PC1ROT, PC2ROTA, PC2ROTB listed in order # of the PC1 rotation schedule, as used by des_setkey - #--------------------------------------------------- + #--------------------------------------------------------------- ##ROTATES = (1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1) ##PCXROT = ( ## PC1ROT, PC2ROTA, PC2ROTB, PC2ROTB, @@ -299,8 +306,8 @@ ## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTA, ## ) - #NOTE: modified PCXROT to contain entrys broken into pairs, - # to help generate them in format best used by encoder. + # NOTE: modified PCXROT to contain entrys broken into pairs, + # to help generate them in format best used by encoder. PCXROT = ( (PC1ROT, PC2ROTA), (PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTB), @@ -308,11 +315,11 @@ (PC2ROTB, PC2ROTB), (PC2ROTB, PC2ROTA), ) - #--------------------------------------------------- + #--------------------------------------------------------------- # Bit reverse, intial permupation, expantion # Initial permutation/expansion table - #--------------------------------------------------- - #NOTE: this was reordered from original table to make perm3264 logic simpler + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm3264 logic simpler IE3264=( ( 0x0000000000000000, 0x0000000000800800, 0x0000000000008008, 0x0000000000808808, 0x0000008008000000, 0x0000008008800800, 0x0000008008008008, 0x0000008008808808, @@ -348,9 +355,9 @@ 0x4044040000000000, 0x4044440400000000, 0x4044044004000000, 0x4044444404000000, ), ) - #--------------------------------------------------- + #--------------------------------------------------------------- # Table that combines the S, P, and E operations. - #--------------------------------------------------- + #--------------------------------------------------------------- SPE=( ( 0x0080088008200000, 0x0000008008000000, 0x0000000000200020, 0x0080088008200020, 0x0000000000200000, 0x0080088008000020, 0x0000008008000020, 0x0000000000200020, @@ -482,11 +489,11 @@ 0x0000000000002000, 0x8008000080082000, 0x0000002000000000, 0x8008002080080000, ), ) - #--------------------------------------------------- + #--------------------------------------------------------------- # compressed/interleaved => final permutation table # Compression, final permutation, bit reverse - #--------------------------------------------------- - #NOTE: this was reordered from original table to make perm6464 logic simpler + #--------------------------------------------------------------- + # NOTE: this was reordered from original table to make perm6464 logic simpler CF6464=( ( 0x0000000000000000, 0x0000002000000000, 0x0000200000000000, 0x0000202000000000, 0x0020000000000000, 0x0020002000000000, 0x0020200000000000, 0x0020202000000000, @@ -553,122 +560,233 @@ 0x0000000004000000, 0x0000000004000004, 0x0000000004000400, 0x0000000004000404, 0x0000000004040000, 0x0000000004040004, 0x0000000004040400, 0x0000000004040404, ), ) - #========================================================= - #eof load_data - #========================================================= + #=================================================================== + # eof _load_tables() + #=================================================================== + +#============================================================================= +# support +#============================================================================= -def permute(c, p): +def _permute(c, p): """Returns the permutation of the given 32-bit or 64-bit code with the specified permutation table.""" - #NOTE: only difference between 32 & 64 bit permutations - #is that len(p)==8 for 32 bit, and len(p)==16 for 64 bit. + # NOTE: only difference between 32 & 64 bit permutations + # is that len(p)==8 for 32 bit, and len(p)==16 for 64 bit. out = 0 for r in p: out |= r[c&0xf] c >>= 4 return out -#========================================================= -#des frontend -#========================================================= -def expand_des_key(key): - "convert 7 byte des key to 8 byte des key (by adding parity bit every 7 bits)" - if not isinstance(key, bytes): - raise TypeError("key must be bytes, not %s" % (type(key),)) - - #NOTE: could probably do this much more cleverly and efficiently, - # but no need really given it's use. - - #NOTE: the parity bits are generally ignored, including by des_encrypt_block below - assert len(key) == 7 - - def iter_bits(source): - for c in source: - v = bord(c) - for i in xrange(7,-1,-1): - yield (v>>i) & 1 +#============================================================================= +# packing & unpacking +#============================================================================= +_uint64_struct = struct.Struct(">Q") - out = 0 - p = 1 - for i, b in enumerate(iter_bits(key)): - out = (out<<1) + b - p ^= b - if i % 7 == 6: - out = (out<<1) + p - p = 1 - - return bjoin_ints( - ((out>>s) & 0xFF) - for s in xrange(8*7,-8,-8) - ) +_BNULL = b('\x00') + +def _pack64(value): + return _uint64_struct.pack(value) + +def _unpack64(value): + return _uint64_struct.unpack(value)[0] + +def _pack56(value): + return _uint64_struct.pack(value)[1:] -def des_encrypt_block(key, input): - """do traditional encryption of a single DES block +def _unpack56(value): + return _uint64_struct.unpack(_BNULL+value)[0] - :arg key: 8 byte des key - :arg input: 8 byte plaintext - :returns: 8 byte ciphertext +#============================================================================= +# 56->64 key manipulation +#============================================================================= - all values must be :class:`bytes` +##def expand_7bit(value): +## "expand 7-bit integer => 7-bits + 1 odd-parity bit" +## # parity calc adapted from 32-bit even parity alg found at +## # http://graphics.stanford.edu/~seander/bithacks.html#ParityParallel +## assert 0 <= value < 0x80, "value out of range" +## return (value<<1) | (0x9669 >> ((value ^ (value >> 4)) & 0xf)) & 1 + +_EXPAND_ITER = irange(49,-7,-7) + +def expand_des_key(key): + "convert DES from 7 bytes to 8 bytes (by inserting empty parity bits)" + if isinstance(key, bytes): + if len(key) != 7: + raise ValueError("key must be 7 bytes in size") + elif isinstance(key, int_types): + if key < 0 or key > INT_56_MASK: + raise ValueError("key must be 56-bit non-negative integer") + return _unpack64(expand_des_key(_pack56(key))) + else: + raise exc.ExpectedTypeError(key, "bytes or int", "key") + key = _unpack56(key) + # NOTE: the following would insert correctly-valued parity bits in each key, + # but the parity bit would just be ignored in des_encrypt_block(), + # so not bothering to use it. + ##return join_byte_values(expand_7bit((key >> shift) & 0x7f) + ## for shift in _EXPAND_ITER) + return join_byte_values(((key>>shift) & 0x7f)<<1 for shift in _EXPAND_ITER) + +def shrink_des_key(key): + "convert DES key from 8 bytes to 7 bytes (by discarding the parity bits)" + if isinstance(key, bytes): + if len(key) != 8: + raise ValueError("key must be 8 bytes in size") + return _pack56(shrink_des_key(_unpack64(key))) + elif isinstance(key, int_types): + if key < 0 or key > INT_64_MASK: + raise ValueError("key must be 64-bit non-negative integer") + else: + raise exc.ExpectedTypeError(key, "bytes or int", "key") + key >>= 1 + result = 0 + offset = 0 + while offset < 56: + result |= (key & 0x7f)<>= 8 + offset += 7 + assert not (result & ~INT_64_MASK) + return result + +#============================================================================= +# des encryption +#============================================================================= +def des_encrypt_block(key, input, salt=0, rounds=1): + """encrypt single block of data using DES, operates on 8-byte strings. + + :arg key: + DES key as 7 byte string, or 8 byte string with parity bits + (parity bit values are ignored). + + :arg input: + plaintext block to encrypt, as 8 byte string. + + :arg salt: + Optional 24-bit integer used to mutate the base DES algorithm in a + manner specific to :class:`~passlib.hash.des_crypt` and it's variants. + The default value ``0`` provides the normal (unsalted) DES behavior. + The salt functions as follows: + if the ``i``'th bit of ``salt`` is set, + bits ``i`` and ``i+24`` are swapped in the DES E-box output. + + :arg rounds: + Optional number of rounds of to apply the DES key schedule. + the default (``rounds=1``) provides the normal DES behavior, + but :class:`~passlib.hash.des_crypt` and it's variants use + alternate rounds values. + + :raises TypeError: if any of the provided args are of the wrong type. + :raises ValueError: + if any of the input blocks are the wrong size, + or the salt/rounds values are out of range. + + :returns: + resulting 8-byte ciphertext block. """ - if not isinstance(key, bytes): - raise TypeError("key must be bytes, not %s" % (type(key),)) - if len(key) == 7: - key = expand_des_key(key) - assert len(key) == 8 - if not isinstance(input, bytes): - raise TypeError("input must be bytes, not %s" % (type(input),)) - assert len(input) == 8 - input = bytes_to_int(input) - key = bytes_to_int(key) - out = mdes_encrypt_int_block(key, input, 0, 1) - return int_to_bytes(out, 8) - -def mdes_encrypt_int_block(key, input, salt=0, rounds=1): - """do modified multi-round DES encryption of single DES block. - - the function implements the salted, variable-round version - of DES used by :class:`~passlib.hash.des_crypt` and related variants. - it also can perform regular DES encryption - by using ``salt=0, rounds=1`` (the default values). - - :arg key: 8 byte des key as integer - :arg input: 8 byte plaintext block as integer - :arg salt: integer 24 bit salt, used to mutate output (defaults to 0) - :arg rounds: number of rounds of DES encryption to apply (defaults to 1) - - The salt is used to to mutate the normal DES encrypt operation - by swapping bits ``i`` and ``i+24`` in the DES E-Box output - if and only if bit ``i`` is set in the salt value. Thus, - if the salt is set to ``0``, normal DES encryption is performed. + # validate & unpack key + if isinstance(key, bytes): + if len(key) == 7: + key = expand_des_key(key) + elif len(key) != 8: + raise ValueError("key must be 7 or 8 bytes") + key = _unpack64(key) + else: + raise exc.ExpectedTypeError(key, "bytes", "key") + + # validate & unpack input + if isinstance(input, bytes): + if len(input) != 8: + raise ValueError("input block must be 8 bytes") + input = _unpack64(input) + else: + raise exc.ExpectedTypeError(input, "bytes", "input") + + # hand things off to other func + result = des_encrypt_int_block(key, input, salt, rounds) + + # repack result + return _pack64(result) + +def des_encrypt_int_block(key, input, salt=0, rounds=1): + """encrypt single block of data using DES, operates on 64-bit integers. + + this function is essentially the same as :func:`des_encrypt_block`, + except that it operates on integers, and will NOT automatically + expand 56-bit keys if provided (since there's no way to detect them). + + :arg key: + DES key as 64-bit integer (the parity bits are ignored). + + :arg input: + input block as 64-bit integer + + :arg salt: + optional 24-bit integer used to mutate the base DES algorithm. + defaults to ``0`` (no mutation applied). + + :arg rounds: + optional number of rounds of to apply the DES key schedule. + defaults to ``1``. + + :raises TypeError: if any of the provided args are of the wrong type. + :raises ValueError: + if any of the input blocks are the wrong size, + or the salt/rounds values are out of range. :returns: - resulting block as 8 byte integer + resulting ciphertext as 64-bit integer. """ + #--------------------------------------------------------------- + # input validation + #--------------------------------------------------------------- + + # validate salt, rounds + if rounds < 1: + raise ValueError("rounds must be positive integer") + if salt < 0 or salt > INT_24_MASK: + raise ValueError("salt must be 24-bit non-negative integer") + + # validate & unpack key + if not isinstance(key, int_types): + raise exc.ExpectedTypeError(key, "int", "key") + elif key < 0 or key > INT_64_MASK: + raise ValueError("key must be 64-bit non-negative integer") + + # validate & unpack input + if not isinstance(input, int_types): + raise exc.ExpectedTypeError(input, "int", "input") + elif input < 0 or input > INT_64_MASK: + raise ValueError("input must be 64-bit non-negative integer") + + #--------------------------------------------------------------- + # DES setup + #--------------------------------------------------------------- + # load tables if not already done global SPE, PCXROT, IE3264, CF6464 + if PCXROT is None: + _load_tables() - #bounds check - assert 0 <= input <= INT_64_MAX, "input value out of range" - assert 0 <= salt <= INT_24_MAX, "salt value out of range" - assert rounds >= 0, "rounds out of range" - assert 0 <= key <= INT_64_MAX, "key value out of range" + # load SPE into local vars to speed things up and remove an array access call + SPE0, SPE1, SPE2, SPE3, SPE4, SPE5, SPE6, SPE7 = SPE - #load tables if not already done - if PCXROT is None: - load_tables() + # NOTE: parity bits are ignored completely + # (UTs do fuzz testing to ensure this) - #convert key int -> key schedule - #NOTE: generation was modified to output two elements at a time, - #to optimize for per-round algorithm below. - mask = ~0x0303030300000000 - def _gen(K): + # generate key schedule + # NOTE: generation was modified to output two elements at a time, + # so that per-round loop could do two passes at once. + def _iter_key_schedule(ks_odd): + "given 64-bit key, iterates over the 8 (even,odd) key schedule pairs" for p_even, p_odd in PCXROT: - K1 = permute(K, p_even) - K = permute(K1, p_odd) - yield K1 & mask, K & mask - ks_list = list(_gen(key)) + ks_even = _permute(ks_odd, p_even) + ks_odd = _permute(ks_even, p_odd) + yield ks_even & _KS_MASK, ks_odd & _KS_MASK + ks_list = list(_iter_key_schedule(key)) - #expand 24 bit salt -> 32 bit + # expand 24 bit salt -> 32 bit per des_crypt & bsdi_crypt salt = ( ((salt & 0x00003f) << 26) | ((salt & 0x000fc0) << 12) | @@ -676,26 +794,25 @@ ((salt & 0xfc0000) >> 16) ) - #init L & R + # init L & R if input == 0: L = R = 0 else: L = ((input >> 31) & 0xaaaaaaaa) | (input & 0x55555555) - L = permute(L, IE3264) + L = _permute(L, IE3264) R = ((input >> 32) & 0xaaaaaaaa) | ((input >> 1) & 0x55555555) - R = permute(R, IE3264) + R = _permute(R, IE3264) - #load SPE into local vars to speed things up and remove an array access call - SPE0, SPE1, SPE2, SPE3, SPE4, SPE5, SPE6, SPE7 = SPE - - #run specified number of passed + #--------------------------------------------------------------- + # main DES loop - run for specified number of rounds + #--------------------------------------------------------------- while rounds: rounds -= 1 - #run over each part of the schedule, 2 parts at a time + # run over each part of the schedule, 2 parts at a time for ks_even, ks_odd in ks_list: - k = ((R>>32) ^ R) & salt #use the salt to alter specific bits + k = ((R>>32) ^ R) & salt # use the salt to flip specific bits B = (k<<32) ^ k ^ R ^ ks_even L ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^ @@ -703,7 +820,7 @@ SPE4[(B>>26)&0x3f] ^ SPE5[(B>>18)&0x3f] ^ SPE6[(B>>10)&0x3f] ^ SPE7[(B>>2)&0x3f]) - k = ((L>>32) ^ L) & salt #use the salt to alter specific bits + k = ((L>>32) ^ L) & salt # use the salt to flip specific bits B = (k<<32) ^ k ^ L ^ ks_odd R ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^ @@ -714,6 +831,9 @@ # swap L and R L, R = R, L + #--------------------------------------------------------------- + # return final result + #--------------------------------------------------------------- C = ( ((L>>3) & 0x0f0f0f0f00000000) | @@ -723,11 +843,17 @@ | ((R<<1) & 0x00000000f0f0f0f0) ) + return _permute(C, CF6464) - C = permute(C, CF6464) - - return C - -#========================================================= -#eof -#========================================================= +@deprecated_function(deprecated="1.6", removed="1.8", + replacement="des_encrypt_int_block()") +def mdes_encrypt_int_block(key, input, salt=0, rounds=1): # pragma: no cover -- deprecated & unused + if isinstance(key, bytes): + if len(key) == 7: + key = expand_des_key(key) + key = _unpack64(key) + return des_encrypt_int_block(key, input, salt, rounds) + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/h64.py passlib-1.6.1/passlib/utils/h64.py --- passlib-1.5.3/passlib/utils/h64.py 2011-10-08 04:51:13.000000000 +0000 +++ passlib-1.6.1/passlib/utils/h64.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,288 +0,0 @@ -"""passlib.utils.h64 - hash64 encoding helpers""" -#================================================================================= -#imports -#================================================================================= -#core -import logging; log = logging.getLogger(__name__) -#site -#pkg -from passlib.utils import bytes, bjoin, bchrs, bord, belem_join -#local -__all__ = [ - "CHARS", - - "decode_bytes", "encode_bytes", - "decode_transposed_bytes", "encode_transposed_bytes", - - "decode_int6", "encode_int6", - "decode_int12", "encode_int12" - "decode_int18", "encode_int18" - "decode_int24", "encode_int24", - "decode_int64", "encode_int64", - "decode_int", "encode_int", -] - -#================================================================================= -#6 bit value <-> char mapping, and other internal helpers -#================================================================================= - -#: hash64 char sequence -CHARS = u"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" -BCHARS = CHARS.encode("ascii") - -#: encode int -> hash64 char as efficiently as possible, w/ minimal checking -# Py2k # -_encode_6bit = BCHARS.__getitem__ -# Py3k # -#_encode_6bit = lambda v: BCHARS[v:v+1] -# end Py3k # - - -#: decode hash64 char -> int as efficiently as possible, w/ minimal checking -_CHARIDX = dict((_encode_6bit(i),i) for i in xrange(64)) -_decode_6bit = _CHARIDX.__getitem__ # char -> int - -#for py3, enhance _CHARIDX to also support int value of bytes -# Py3k # -#_CHARIDX.update((v,i) for i,v in enumerate(BCHARS)) -# end Py3k # - -#================================================================================= -#encode offsets from buffer - used by md5_crypt, sha_crypt, et al -#================================================================================= - -def _encode_bytes_helper(source): - #FIXME: do something much more efficient here. - # can't quite just use base64 and then translate chars, - # since this scheme is little-endian. - end = len(source) - tail = end % 3 - end -= tail - idx = 0 - while idx < end: - v1 = bord(source[idx]) - v2 = bord(source[idx+1]) - v3 = bord(source[idx+2]) - yield encode_int24(v1 + (v2<<8) + (v3<<16)) - idx += 3 - if tail: - v1 = bord(source[idx]) - if tail == 1: - #NOTE: 4 msb of int are always 0 - yield encode_int12(v1) - else: - #NOTE: 2 msb of int are always 0 - v2 = bord(source[idx+1]) - yield encode_int18(v1 + (v2<<8)) - -def encode_bytes(source): - "encode byte string to h64 format" - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - return bjoin(_encode_bytes_helper(source)) - -def _decode_bytes_helper(source): - end = len(source) - tail = end % 4 - if tail == 1: - #only 6 bits left, can't encode a whole byte! - raise ValueError("input string length cannot be == 1 mod 4") - end -= tail - idx = 0 - while idx < end: - v = decode_int24(source[idx:idx+4]) - yield bchrs(v&0xff, (v>>8)&0xff, v>>16) - idx += 4 - if tail: - if tail == 2: - #NOTE: 2 msb of int are ignored (should be 0) - v = decode_int12(source[idx:idx+2]) - yield bchrs(v&0xff) - else: - #NOTE: 4 msb of int are ignored (should be 0) - v = decode_int18(source[idx:idx+3]) - yield bchrs(v&0xff, (v>>8)&0xff) - -def decode_bytes(source): - "decode h64 format into byte string" - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - return bjoin(_decode_bytes_helper(source)) - -def encode_transposed_bytes(source, offsets): - "encode byte string to h64 format, using offset list to transpose elements" - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - #XXX: could make this a dup of encode_bytes(), which directly accesses source[offsets[idx]], - # but speed isn't *that* critical for this function - tmp = belem_join(source[off] for off in offsets) - return encode_bytes(tmp) - -def decode_transposed_bytes(source, offsets): - "decode h64 format into byte string, then undoing specified transposition; inverse of :func:`encode_transposed_bytes`" - #NOTE: if transposition does not use all bytes of source, original can't be recovered - tmp = decode_bytes(source) - buf = [None] * len(offsets) - for off, char in zip(offsets, tmp): - buf[off] = char - return belem_join(buf) - -#================================================================================= -# int <-> b64 string, used by des_crypt, bsdi_crypt -#================================================================================= - -def decode_int6(source): - "decodes single hash64 character -> 6-bit integer" - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - try: - return _decode_6bit(source) - except KeyError: - raise ValueError("invalid character") - -def encode_int6(value): - "encodes 6-bit integer -> single hash64 character" - if value < 0 or value > 63: - raise ValueError("value out of range") - return _encode_6bit(value) - -#--------------------------------------------------------------------- - -def decode_int12(source): - "decodes 2 char hash64 string -> 12-bit integer (little-endian order)" - #NOTE: this is optimized form of decode_int(value) for 4 chars - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - try: - return (_decode_6bit(source[1])<<6)+_decode_6bit(source[0]) - except KeyError: - raise ValueError("invalid character") - -def encode_int12(value): - "encodes 12-bit integer -> 2 char hash64 string (little-endian order)" - #NOTE: this is optimized form of encode_int(value,2) - return _encode_6bit(value & 0x3f) + _encode_6bit((value>>6) & 0x3f) - -#--------------------------------------------------------------------- -def decode_int18(source): - "decodes 3 char hash64 string -> 18-bit integer (little-endian order)" - #NOTE: this is optimized form of decode_int(value) for 3 chars - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - return ( - _decode_6bit(source[0]) + - (_decode_6bit(source[1])<<6) + - (_decode_6bit(source[2])<<12) - ) - -def encode_int18(value): - "encodes 18-bit integer -> 3 char hash64 string (little-endian order)" - #NOTE: this is optimized form of encode_int(value,3) - return ( - _encode_6bit(value & 0x3f) + - _encode_6bit((value>>6) & 0x3f) + - _encode_6bit((value>>12) & 0x3f) - ) - -#--------------------------------------------------------------------- - -def decode_int24(source): - "decodes 4 char hash64 string -> 24-bit integer (little-endian order)" - #NOTE: this is optimized form of decode_int(source) for 4 chars - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - try: - return _decode_6bit(source[0]) +\ - (_decode_6bit(source[1])<<6)+\ - (_decode_6bit(source[2])<<12)+\ - (_decode_6bit(source[3])<<18) - except KeyError: - raise ValueError("invalid character") - -def encode_int24(value): - "encodes 24-bit integer -> 4 char hash64 string (little-endian order)" - #NOTE: this is optimized form of encode_int(value,4) - return _encode_6bit(value & 0x3f) + \ - _encode_6bit((value>>6) & 0x3f) + \ - _encode_6bit((value>>12) & 0x3f) + \ - _encode_6bit((value>>18) & 0x3f) - -#--------------------------------------------------------------------- - -def decode_int64(source): - "decodes 11 char hash64 string -> 64-bit integer (little-endian order; 2 msb assumed to be padding)" - return decode_int(source) - -def encode_int64(value): - "encodes 64-bit integer -> 11 char hash64 string (little-endian order; 2 msb of 0's added as padding)" - return encode_int(value, 11) - -def decode_dc_int64(source): - """decode 11 char hash64 string -> 64-bit integer (big-endian order; 2 lsb assumed to be padding) - - this format is used primarily by des-crypt & variants to encode the DES output value - used as a checksum. - """ - return decode_int(source, True)>>2 - -def encode_dc_int64(value): - """encode 64-bit integer -> 11 char hash64 string (big-endian order; 2 lsb added as padding) - - this format is used primarily by des-crypt & variants to encode the DES output value - used as a checksum. - """ - #NOTE: insert 2 padding bits as lsb, to make 66 bits total - return encode_int(value<<2,11,True) - -#--------------------------------------------------------------------- - -def decode_int(source, big=False): - """decode hash64 string -> integer - - :arg source: hash64 string of any length - :arg big: if ``True``, big-endian encoding is used instead of little-endian (the default). - - :raises ValueError: if the string contains invalid hash64 characters. - - :returns: - a integer whose value is in ``range(0,2**(6*len(source)))`` - """ - if not isinstance(source, bytes): - raise TypeError("source must be bytes, not %s" % (type(source),)) - #FORMAT: little-endian, each char contributes 6 bits, - # char value = index in H64_CHARS string - if not big: - source = reversed(source) - try: - out = 0 - for c in source: - #NOTE: under py3, 'c' is int, relying on _CHARIDX to support this. - out = (out<<6) + _decode_6bit(c) - return out - except KeyError: - raise ValueError("invalid character in string") - -def encode_int(value, count, big=False): - """encode integer into hash-64 format - - :arg value: non-negative integer to encode - :arg count: number of output characters / 6 bit chunks to encode - :arg big: if ``True``, big-endian encoding is used instead of little-endian (the default). - - :returns: - a hash64 string of length ``count``. - """ - if value < 0: - raise ValueError("value cannot be negative") - if big: - itr = xrange(6*count-6, -6, -6) - else: - itr = xrange(0, 6*count, 6) - return bjoin( - _encode_6bit((value>>off) & 0x3f) - for off in itr - ) - -#================================================================================= -#eof -#================================================================================= diff -Nru passlib-1.5.3/passlib/utils/handlers.py passlib-1.6.1/passlib/utils/handlers.py --- passlib-1.5.3/passlib/utils/handlers.py 2011-10-08 04:56:58.000000000 +0000 +++ passlib-1.6.1/passlib/utils/handlers.py 2012-08-01 17:14:10.000000000 +0000 @@ -1,9 +1,9 @@ """passlib.handler - code for implementing handlers, and global registry for handlers""" -#========================================================= -#imports -#========================================================= +#============================================================================= +# imports +#============================================================================= from __future__ import with_statement -#core +# core import inspect import re import hashlib @@ -11,81 +11,126 @@ import time import os from warnings import warn -#site -#libs +# site +# pkg +import passlib.exc as exc +from passlib.exc import MissingBackendError, PasslibConfigWarning, \ + PasslibHashWarning +from passlib.ifc import PasswordHash from passlib.registry import get_crypt_handler -from passlib.utils import to_hash_str, bytes, b, \ - classproperty, h64, getrandstr, getrandbytes, \ - rng, is_crypt_handler, ALL_BYTE_VALUES, MissingBackendError -#pkg -#local +from passlib.utils import classproperty, consteq, getrandstr, getrandbytes,\ + BASE64_CHARS, HASH64_CHARS, rng, to_native_str, \ + is_crypt_handler, to_unicode, \ + MAX_PASSWORD_SIZE +from passlib.utils.compat import b, join_byte_values, bytes, irange, u, \ + uascii_to_str, join_unicode, unicode, str_to_uascii, \ + join_unicode, base_string_types, PY2, int_types +# local __all__ = [ + # helpers for implementing MCF handlers + 'parse_mc2', + 'parse_mc3', + 'render_mc2', + 'render_mc3', - #framework for implementing handlers - 'StaticHandler', + # framework for implementing handlers 'GenericHandler', + 'StaticHandler', + 'HasUserContext', 'HasRawChecksum', 'HasManyIdents', 'HasSalt', - 'HasRawSalt', + 'HasRawSalt', 'HasRounds', 'HasManyBackends', + + # other helpers 'PrefixWrapper', ] -#========================================================= -#constants -#========================================================= - -#common salt_chars & checksum_chars values -H64_CHARS = h64.CHARS -B64_CHARS = u"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -PADDED_B64_CHARS = B64_CHARS + u"=" -U64_CHARS = u"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" -HEX_CHARS = u"0123456789abcdefABCDEF" -UC_HEX_CHARS = u"0123456789ABCDEF" -LC_HEX_CHARS = u"0123456789abcdef" - -#========================================================= -#identify helpers -#========================================================= -def identify_regexp(hash, pat): - "identify() helper for matching regexp" - if not hash: - return False - if isinstance(hash, bytes): - try: - hash = hash.decode("ascii") - except UnicodeDecodeError: - return False - return pat.match(hash) is not None +#============================================================================= +# constants +#============================================================================= + +# common salt_chars & checksum_chars values +# (BASE64_CHARS, HASH64_CHARS imported above) +PADDED_BASE64_CHARS = BASE64_CHARS + u("=") +HEX_CHARS = u("0123456789abcdefABCDEF") +UPPER_HEX_CHARS = u("0123456789ABCDEF") +LOWER_HEX_CHARS = u("0123456789abcdef") + +# special byte string containing all possible byte values +# XXX: treated as singleton by some of the code for efficiency. +ALL_BYTE_VALUES = join_byte_values(irange(256)) + +# deprecated aliases - will be removed after passlib 1.8 +H64_CHARS = HASH64_CHARS +B64_CHARS = BASE64_CHARS +PADDED_B64_CHARS = PADDED_BASE64_CHARS +UC_HEX_CHARS = UPPER_HEX_CHARS +LC_HEX_CHARS = LOWER_HEX_CHARS + +#============================================================================= +# support functions +#============================================================================= +def _bitsize(count, chars): + """helper for bitsize() methods""" + if chars and count: + import math + return int(count * math.log(len(chars), 2)) + else: + return 0 -def identify_prefix(hash, prefix): - "identify() helper for matching against prefixes" - #NOTE: prefix may be a tuple of strings (since startswith supports that) - if not hash: - return False - if isinstance(hash, bytes): +#============================================================================= +# parsing helpers +#============================================================================= +_UDOLLAR = u("$") +_UZERO = u("0") + +def validate_secret(secret): + "ensure secret has correct type & size" + if not isinstance(secret, base_string_types): + raise exc.ExpectedStringError(secret, "secret") + if len(secret) > MAX_PASSWORD_SIZE: + raise exc.PasswordSizeError() + +def to_unicode_for_identify(hash): + "convert hash to unicode for identify method" + if isinstance(hash, unicode): + return hash + elif isinstance(hash, bytes): + # try as utf-8, but if it fails, use foolproof latin-1, + # since we don't really care about non-ascii chars + # when running identify. try: - hash = hash.decode("ascii") + return hash.decode("utf-8") except UnicodeDecodeError: - return False - return hash.startswith(prefix) + return hash.decode("latin-1") + else: + raise exc.ExpectedStringError(hash, "hash") + +def parse_mc2(hash, prefix, sep=_UDOLLAR, handler=None): + """parse hash using 2-part modular crypt format. + + this expects a hash of the format :samp:`{prefix}{salt}[${checksum}]`, + such as md5_crypt, and parses it into salt / checksum portions. + + :arg hash: the hash to parse (bytes or unicode) + :arg prefix: the identifying prefix (unicode) + :param sep: field separator (unicode, defaults to ``$``). + :param handler: handler class to pass to error constructors. -#========================================================= -#parsing helpers -#========================================================= -def parse_mc2(hash, prefix, name="", sep=u"$"): - "parse hash using 2-part modular crypt format" + :returns: + a ``(salt, chk | None)`` tuple. + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") assert isinstance(prefix, unicode) - assert isinstance(sep, unicode) - #eg: MD5-Crypt: $1$salt[$checksum] - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode('ascii') if not hash.startswith(prefix): - raise ValueError("not a valid %s hash (wrong prefix)" % (name,)) + raise exc.InvalidHashError(handler) + + # parse 2-part hash or 1-part config string + assert isinstance(sep, unicode) parts = hash[len(prefix):].split(sep) if len(parts) == 2: salt, chk = parts @@ -93,178 +138,153 @@ elif len(parts) == 1: return parts[0], None else: - raise ValueError("not a valid %s hash (malformed)" % (name,)) + raise exc.MalformedHashError(handler) -def parse_mc3(hash, prefix, name="", sep=u"$"): - "parse hash using 3-part modular crypt format" +def parse_mc3(hash, prefix, sep=_UDOLLAR, rounds_base=10, + default_rounds=None, handler=None): + """parse hash using 3-part modular crypt format. + + this expects a hash of the format :samp:`{prefix}[{rounds}]${salt}[${checksum}]`, + such as sha1_crypt, and parses it into rounds / salt / checksum portions. + tries to convert the rounds to an integer, + and throws error if it has zero-padding. + + :arg hash: the hash to parse (bytes or unicode) + :arg prefix: the identifying prefix (unicode) + :param sep: field separator (unicode, defaults to ``$``). + :param rounds_base: + the numeric base the rounds are encoded in (defaults to base 10). + :param default_rounds: + the default rounds value to return if the rounds field was omitted. + if this is ``None`` (the default), the rounds field is *required*. + :param handler: handler class to pass to error constructors. + + :returns: + a ``(rounds : int, salt, chk | None)`` tuple. + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") assert isinstance(prefix, unicode) - assert isinstance(sep, unicode) - #eg: SHA1-Crypt: $sha1$rounds$salt[$checksum] - if not hash: - raise ValueError("no hash specified") - if isinstance(hash, bytes): - hash = hash.decode('ascii') if not hash.startswith(prefix): - raise ValueError("not a valid %s hash" % (name,)) + raise exc.InvalidHashError(handler) + + # parse 3-part hash or 2-part config string + assert isinstance(sep, unicode) parts = hash[len(prefix):].split(sep) if len(parts) == 3: rounds, salt, chk = parts - return rounds, salt, chk or None elif len(parts) == 2: rounds, salt = parts - return rounds, salt, None - else: - raise ValueError("not a valid %s hash" % (name,)) - -#===================================================== -#formatting helpers -#===================================================== -def render_mc2(ident, salt, checksum, sep=u"$"): - "format hash using 2-part modular crypt format; inverse of parse_mc2" - if checksum: - hash = u"%s%s%s%s" % (ident, salt, sep, checksum) + chk = None else: - hash = u"%s%s" % (ident, salt) - return to_hash_str(hash) + raise exc.MalformedHashError(handler) -def render_mc3(ident, rounds, salt, checksum, sep=u"$"): - "format hash using 3-part modular crypt format; inverse of parse_mc3" - if checksum: - hash = u"%s%s%s%s%s%s" % (ident, rounds, sep, salt, sep, checksum) + # validate & parse rounds portion + if rounds.startswith(_UZERO) and rounds != _UZERO: + raise exc.ZeroPaddedRoundsError(handler) + elif rounds: + rounds = int(rounds, rounds_base) + elif default_rounds is None: + raise exc.MalformedHashError(handler, "empty rounds field") else: - hash = u"%s%s%s%s" % (ident, rounds, sep, salt) - return to_hash_str(hash) + rounds = default_rounds -#===================================================== -#StaticHandler -#===================================================== -class StaticHandler(object): - """helper class for implementing hashes which have no settings. - - This class is designed to help in writing hash handlers - which have no settings whatsoever; that is to say: no salt, no rounds, etc. - These hashes can typically be recognized by the fact that they - will always hash a password to *exactly* the same hash string. - - Usage - ===== - - In order to use this class, just subclass it, and then do the following: - - * fill out the :attr:`name` attribute with the name of your hash. - * provide an implementation of the :meth:`~PasswordHash.genhash` method. - * provide an implementation of the :meth:`~PasswordHash.identify` method. - (a default is provided, but it's inefficient). - - Based on the methods above, this class provides: - - * a :meth:`genconfig` method that returns ``None``. - * a :meth:`encrypt` method that wraps :meth:`genhash`. - * a :meth:`verify` method that wraps :meth:`genhash`. - - Implementation Details - ====================== - - The :meth:`genhash` method you implement must accept - all valid hashes, *as well as* whatever value :meth:`genconfig` returns. - This defaults to ``None``, but you may set the :attr:`_stub_config` attr - to a random hash string, and :meth:`genconfig` will return this instead. - - The default :meth:`verify` method uses simple equality to compare hash strings. - If your hash may have multiple encoding (eg case-insensitive), this - method (or the private :meth:`_norm_hash` method) - should be overridden on a per-handler basis. - - If your hash has options, such as multiple identifiers, salts, - or variable rounds, this is not the right class to start with. - You should use the :class:`GenericHandler` class, or implement the handler yourself. - """ - - #===================================================== - #class attrs - #===================================================== - name = None #required - handler name - setting_kwds = () - context_kwds = () + # return result + return rounds, salt, chk or None - _stub_config = None +#============================================================================= +# formatting helpers +#============================================================================= +def render_mc2(ident, salt, checksum, sep=u("$")): + """format hash using 2-part modular crypt format; inverse of parse_mc2() + + returns native string with format :samp:`{ident}{salt}[${checksum}]`, + such as used by md5_crypt. + + :arg ident: identifier prefix (unicode) + :arg salt: encoded salt (unicode) + :arg checksum: encoded checksum (unicode or None) + :param sep: separator char (unicode, defaults to ``$``) - #===================================================== - #methods - #===================================================== - @classmethod - def identify(cls, hash): - #NOTE: this relys on genhash() throwing error for invalid hashes. - # this approach is bad because genhash may take a long time on valid hashes, - # so subclasses *really* should override this. - try: - cls.genhash('fakesecret', hash) - return True - except ValueError: - return False - - @classmethod - def genconfig(cls): - return cls._stub_config + :returns: + config or hash (native str) + """ + if checksum: + parts = [ident, salt, sep, checksum] + else: + parts = [ident, salt] + return uascii_to_str(join_unicode(parts)) - @classmethod - def genhash(cls, secret, config, **context): - raise NotImplementedError("%s subclass must implement genhash()" % (cls,)) +def render_mc3(ident, rounds, salt, checksum, sep=u("$"), rounds_base=10): + """format hash using 3-part modular crypt format; inverse of parse_mc3() - @classmethod - def encrypt(cls, secret, *cargs, **context): - #NOTE: subclasses generally won't need to override this. - config = cls.genconfig() - return cls.genhash(secret, config, *cargs, **context) + returns native string with format :samp:`{ident}[{rounds}$]{salt}[${checksum}]`, + such as used by sha1_crypt. - @classmethod - def verify(cls, secret, hash, *cargs, **context): - #NOTE: subclasses generally won't need to override this. - if hash is None: - raise ValueError("no hash specified") - hash = cls._norm_hash(hash) - result = cls.genhash(secret, hash, *cargs, **context) - return cls._norm_hash(result) == hash + :arg ident: identifier prefix (unicode) + :arg rounds: rounds field (int or None) + :arg salt: encoded salt (unicode) + :arg checksum: encoded checksum (unicode or None) + :param sep: separator char (unicode, defaults to ``$``) + :param rounds_base: base to encode rounds value (defaults to base 10) - @classmethod - def _norm_hash(cls, hash): - """[helper for verify] normalize hash for comparsion purposes""" - #NOTE: this is mainly provided for case-insenstive subclasses to override. - if isinstance(hash, bytes): - hash = hash.decode("ascii") - return hash + :returns: + config or hash (native str) + """ + if rounds is None: + rounds = u('') + elif rounds_base == 16: + rounds = u("%x") % rounds + else: + assert rounds_base == 10 + rounds = unicode(rounds) + if checksum: + parts = [ident, rounds, sep, salt, sep, checksum] + else: + parts = [ident, rounds, sep, salt] + return uascii_to_str(join_unicode(parts)) - #===================================================== - #eoc - #===================================================== - -#===================================================== -#GenericHandler -#===================================================== -class GenericHandler(object): +#============================================================================= +# GenericHandler +#============================================================================= +class GenericHandler(PasswordHash): """helper class for implementing hash handlers. + GenericHandler-derived classes will have (at least) the following + constructor options, though others may be added by mixins + and by the class itself: + :param checksum: this should contain the digest portion of a parsed hash (mainly provided when the constructor is called by :meth:`from_string()`). defaults to ``None``. - :param strict: - If ``True``, this flag signals that :meth:`norm_checksum` - (as well as the other :samp:`norm_{xxx}` methods provided by the mixins) - should throw a :exc:`ValueError` if any errors are found - in any of the provided parameters. - - If ``False`` (the default), the :exc:`ValueError` should only - be throw if the error is not recoverable (eg: clipping salt string to max size). + :param use_defaults: + If ``False`` (the default), a :exc:`TypeError` should be thrown + if any settings required by the handler were not explicitly provided. + + If ``True``, the handler should attempt to provide a default for any + missing values. This means generate missing salts, fill in default + cost parameters, etc. This is typically only set to ``True`` when the constructor - is called by :meth:`from_string`, in order to perform validation - on the hash string it's parsing; whereas :meth:`encrypt` - does not set this flag, allowing user-provided values + is called by :meth:`encrypt`, allowing user-provided values to be handled in a more permissive manner. + :param relaxed: + If ``False`` (the default), a :exc:`ValueError` should be thrown + if any settings are out of bounds or otherwise invalid. + + If ``True``, they should be corrected if possible, and a warning + issue. If not possible, only then should an error be raised. + (e.g. under ``relaxed=True``, rounds values will be clamped + to min/max rounds). + + This is mainly used when parsing the config strings of certain + hashes, whose specifications implementations to be tolerant + of incorrect values in salt strings. + Class Attributes ================ @@ -277,6 +297,15 @@ This should be a unicode str. + .. attribute:: _hash_regex + + [optional] + If this attribute is filled in, the default :meth:`identify` method + will use it to recognize instances of the hash. If :attr:`ident` + is specified, this will be ignored. + + This should be a unique regex object. + .. attribute:: checksum_size [optional] @@ -291,26 +320,39 @@ This should be a unicode str. + .. attribute:: _stub_checksum + + [optional] + If specified, hashes with this checksum will have their checksum + normalized to ``None``, treating it like a config string. + This is mainly used by hash formats which don't have a concept + of a config string, so a unlikely-to-occur checksum (e.g. all zeros) + is used by some implementations. + + This should be a string of the same datatype as :attr:`checksum`, + or ``None``. + Instance Attributes =================== .. attribute:: checksum - The checksum string as provided by the constructor (after passing through :meth:`norm_checksum`). + The checksum string provided to the constructor (after passing it + through :meth:`_norm_checksum`). - Required Class Methods - ====================== + Required Subclass Methods + ========================= The following methods must be provided by handler subclass: .. automethod:: from_string .. automethod:: to_string - .. automethod:: calc_checksum + .. automethod:: _calc_checksum - Default Class Methods - ===================== - The following methods provide generally useful default behaviors, - though they may be overridden if the hash subclass needs to: + Default Methods + =============== + The following methods have default implementations that should work for + most cases, though they may be overridden if the hash subclass needs to: - .. automethod:: norm_checksum + .. automethod:: _norm_checksum .. automethod:: genconfig .. automethod:: genhash @@ -319,78 +361,133 @@ .. automethod:: verify """ - #===================================================== - #class attr - #===================================================== + #=================================================================== + # class attr + #=================================================================== + # this must be provided by the actual class. + setting_kwds = None + + # providing default since most classes don't use this at all. context_kwds = () - ident = None #identifier prefix if known + # optional prefix that uniquely identifies hash + ident = None - checksum_size = None #if specified, norm_checksum will require this length - checksum_chars = None #if specified, norm_checksum() will validate this + # optional regexp for recognizing hashes, + # used by default identify() if .ident isn't specified. + _hash_regex = None - #===================================================== - #instance attrs - #===================================================== - checksum = None - - #===================================================== - #init - #===================================================== - def __init__(self, checksum=None, strict=False, **kwds): - self.checksum = self.norm_checksum(checksum, strict=strict) - super(GenericHandler, self).__init__(**kwds) + # if specified, _norm_checksum will require this length + checksum_size = None - #XXX: support a subclass-specified _norm_checksum method - # to normalize for the purposes of verify()? - # currently the code cost seems smaller to just have classes override verify. + # if specified, _norm_checksum() will validate this + checksum_chars = None - @classmethod - def norm_checksum(cls, checksum, strict=False): - "validates checksum keyword against class requirements, returns normalized version of checksum" + # if specified, hashes with this checksum will be treated + # as if no checksum was specified. + _stub_checksum = None + + # private flag used by HasRawChecksum + _checksum_is_bytes = False + + #=================================================================== + # instance attrs + #=================================================================== + checksum = None # stores checksum +# use_defaults = False # whether _norm_xxx() funcs should fill in defaults. +# relaxed = False # when _norm_xxx() funcs should be strict about inputs + + #=================================================================== + # init + #=================================================================== + def __init__(self, checksum=None, use_defaults=False, relaxed=False, + **kwds): + self.use_defaults = use_defaults + self.relaxed = relaxed + super(GenericHandler, self).__init__(**kwds) + self.checksum = self._norm_checksum(checksum) + + def _norm_checksum(self, checksum): + """validates checksum keyword against class requirements, + returns normalized version of checksum. + """ + # NOTE: by default this code assumes checksum should be unicode. + # For classes where the checksum is raw bytes, the HasRawChecksum sets + # the _checksum_is_bytes flag which alters various code paths below. if checksum is None: - if strict: - raise ValueError("checksum not specified") return None - if isinstance(checksum, bytes): - checksum = checksum.decode('ascii') - cc = cls.checksum_size + + # normalize to bytes / unicode + raw = self._checksum_is_bytes + if raw: + # NOTE: no clear route to reasonbly convert unicode -> raw bytes, + # so relaxed does nothing here + if not isinstance(checksum, bytes): + raise exc.ExpectedTypeError(checksum, "bytes", "checksum") + + elif not isinstance(checksum, unicode): + if isinstance(checksum, bytes) and self.relaxed: + warn("checksum should be unicode, not bytes", + PasslibHashWarning) + checksum = checksum.decode("ascii") + else: + raise exc.ExpectedTypeError(checksum, "unicode", "checksum") + + # handle stub + if checksum == self._stub_checksum: + return None + + # check size + cc = self.checksum_size if cc and len(checksum) != cc: - raise ValueError("%s checksum must be %d characters" % (cls.name, cc)) - cs = cls.checksum_chars - if cs and any(c not in cs for c in checksum): - raise ValueError("invalid characters in %s checksum" % (cls.name,)) + raise exc.ChecksumSizeError(self, raw=raw) + + # check charset + if not raw: + cs = self.checksum_chars + if cs and any(c not in cs for c in checksum): + raise ValueError("invalid characters in %s checksum" % + (self.name,)) + return checksum - #===================================================== - #password hash api - formatting interface - #===================================================== + #=================================================================== + # password hash api - formatting interface + #=================================================================== @classmethod def identify(cls, hash): - #NOTE: subclasses may wish to use faster / simpler identify, - # and raise value errors only when an invalid (but identifiable) string is parsed + # NOTE: subclasses may wish to use faster / simpler identify, + # and raise value errors only when an invalid (but identifiable) + # string is parsed + hash = to_unicode_for_identify(hash) if not hash: return False + + # does class specify a known unique prefix to look for? ident = cls.ident - if ident: - #class specified a known prefix to look for - assert isinstance(ident, unicode) - if isinstance(hash, bytes): - ident = ident.encode('ascii') + if ident is not None: return hash.startswith(ident) - else: - #don't have that, so fall back to trying to parse hash - #(inefficient for these purposes) - try: - cls.from_string(hash) - return True - except ValueError: - return False + + # does class provide a regexp to use? + pat = cls._hash_regex + if pat is not None: + return pat.match(hash) is not None + + # as fallback, try to parse hash, and see if we succeed. + # inefficient, but works for most cases. + try: + cls.from_string(hash) + return True + except ValueError: + return False @classmethod - def from_string(cls, hash): #pragma: no cover + def from_string(cls, hash, **context): # pragma: no cover """return parsed instance from hash/configuration string + :param \*\*context: + context keywords to pass to constructor (if applicable). + :raises ValueError: if hash is incorrectly formatted :returns: @@ -399,7 +496,7 @@ """ raise NotImplementedError("%s must implement from_string()" % (cls,)) - def to_string(self): #pragma: no cover + def to_string(self): # pragma: no cover """render instance to hash or configuration string :returns: @@ -410,70 +507,290 @@ should return native string type (ascii-bytes under python 2, unicode under python 3) """ - #NOTE: documenting some non-standardized but common kwd flags - # that passlib to_string() method may have + # NOTE: documenting some non-standardized but common kwd flags + # that passlib to_string() method may have: # - # native=True -- if false, return unicode under py2 -- ignored under py3 - # withchk=True -- if false, omit checksum portion of hash + # withchk=True -- if false, omit checksum portion of hash # - raise NotImplementedError("%s must implement from_string()" % (type(self),)) + raise NotImplementedError("%s must implement from_string()" % + (self.__class__,)) ##def to_config_string(self): ## "helper for generating configuration string (ignoring hash)" - ## chk = self.checksum - ## if chk: - ## try: - ## self.checksum = None - ## return self.to_string() - ## finally: - ## self.checksum = chk - ## else: + ## orig = self.checksum + ## try: + ## self.checksum = None ## return self.to_string() + ## finally: + ## self.checksum = orig - #========================================================= + #=================================================================== #'crypt-style' interface (default implementation) - #========================================================= + #=================================================================== @classmethod def genconfig(cls, **settings): - return cls(**settings).to_string() + return cls(use_defaults=True, **settings).to_string() @classmethod - def genhash(cls, secret, config): - self = cls.from_string(config) - self.checksum = self.calc_checksum(secret) + def genhash(cls, secret, config, **context): + validate_secret(secret) + self = cls.from_string(config, **context) + self.checksum = self._calc_checksum(secret) return self.to_string() - def calc_checksum(self, secret): #pragma: no cover - "given secret; calcuate and return encoded checksum portion of hash string, taking config from object state" - raise NotImplementedError("%s must implement calc_checksum()" % (self.__class__,)) + def _calc_checksum(self, secret): # pragma: no cover + """given secret; calcuate and return encoded checksum portion of hash + string, taking config from object state - #========================================================= + calc checksum implementations may assume secret is always + either unicode or bytes, checks are performed by verify/etc. + """ + raise NotImplementedError("%s must implement _calc_checksum()" % + (self.__class__,)) + + #=================================================================== #'application' interface (default implementation) - #========================================================= + #=================================================================== @classmethod - def encrypt(cls, secret, **settings): - self = cls(**settings) - self.checksum = self.calc_checksum(secret) + def encrypt(cls, secret, **kwds): + validate_secret(secret) + self = cls(use_defaults=True, **kwds) + self.checksum = self._calc_checksum(secret) return self.to_string() @classmethod - def verify(cls, secret, hash): - #NOTE: classes with multiple checksum encodings (rare) - # may wish to either override this, or override norm_checksum - # to normalize any checksums provided by from_string() + def verify(cls, secret, hash, **context): + # NOTE: classes with multiple checksum encodings should either + # override this method, or ensure that from_string() / _norm_checksum() + # ensures .checksum always uses a single canonical representation. + validate_secret(secret) + self = cls.from_string(hash, **context) + chk = self.checksum + if chk is None: + raise exc.MissingDigestError(cls) + return consteq(self._calc_checksum(secret), chk) + + #=================================================================== + # experimental - the following methods are not finished or tested, + # but way work correctly for some hashes + #=================================================================== + _unparsed_settings = ("salt_size", "relaxed") + _unsafe_settings = ("salt", "checksum") + + @classproperty + def _parsed_settings(cls): + return (key for key in cls.setting_kwds + if key not in cls._unparsed_settings) + + @staticmethod + def _sanitize(value, char=u("*")): + "default method to obscure sensitive fields" + if value is None: + return None + if isinstance(value, bytes): + from passlib.utils import ab64_encode + value = ab64_encode(value).decode("ascii") + elif not isinstance(value, unicode): + value = unicode(value) + size = len(value) + clip = min(4, size//8) + return value[:clip] + char * (size-clip) + + @classmethod + def parsehash(cls, hash, checksum=True, sanitize=False): + """[experimental method] parse hash into dictionary of settings. + + this essentially acts as the inverse of :meth:`encrypt`: for most + cases, if ``hash = cls.encrypt(secret, **opts)``, then + ``cls.parsehash(hash)`` will return a dict matching the original options + (with the extra keyword *checksum*). + + this method may not work correctly for all hashes, + and may not be available on some few. it's interface may + change in future releases, if it's kept around at all. + + :arg hash: hash to parse + :param checksum: include checksum keyword? (defaults to True) + :param sanitize: mask data for sensitive fields? (defaults to False) + """ + # FIXME: this may not work for hashes with non-standard settings. + # XXX: how should this handle checksum/salt encoding? + # need to work that out for encrypt anyways. self = cls.from_string(hash) - return self.checksum == self.calc_checksum(secret) + # XXX: could split next few lines out as self._parsehash() for subclassing + # XXX: could try to resolve ident/variant to publically suitable alias. + UNSET = object() + kwds = dict((key, getattr(self, key)) for key in self._parsed_settings + if getattr(self, key) != getattr(cls, key, UNSET)) + if checksum and self.checksum is not None: + kwds['checksum'] = self.checksum + if sanitize: + if sanitize is True: + sanitize = cls._sanitize + for key in cls._unsafe_settings: + if key in kwds: + kwds[key] = sanitize(kwds[key]) + return kwds - #========================================================= - #eoc - #========================================================= + @classmethod + def bitsize(cls, **kwds): + "[experimental method] return info about bitsizes of hash" + try: + info = super(GenericHandler, cls).bitsize(**kwds) + except AttributeError: + info = {} + cc = ALL_BYTE_VALUES if cls._checksum_is_bytes else cls.checksum_chars + if cls.checksum_size and cc: + # FIXME: this may overestimate size due to padding bits (e.g. bcrypt) + # FIXME: this will be off by 1 for case-insensitive hashes. + info['checksum'] = _bitsize(cls.checksum_size, cc) + return info + + #=================================================================== + # eoc + #=================================================================== + +class StaticHandler(GenericHandler): + """GenericHandler mixin for classes which have no settings. + + This mixin assumes the entirety of the hash ise stored in the + :attr:`checksum` attribute; that the hash has no rounds, salt, + etc. This class provides the following: + + * a default :meth:`genconfig` that always returns None. + * a default :meth:`from_string` and :meth:`to_string` + that store the entire hash within :attr:`checksum`, + after optionally stripping a constant prefix. -#===================================================== -#GenericHandler mixin classes -#===================================================== + All that is required by subclasses is an implementation of + the :meth:`_calc_checksum` method. + """ + # TODO: document _norm_hash() -#XXX: add a HasContext helper to override GenericHandler's methods? + setting_kwds = () + # optional constant prefix subclasses can specify + _hash_prefix = u("") + + @classmethod + def from_string(cls, hash, **context): + # default from_string() which strips optional prefix, + # and passes rest unchanged as checksum value. + hash = to_unicode(hash, "ascii", "hash") + hash = cls._norm_hash(hash) + # could enable this for extra strictness + ##pat = cls._hash_regex + ##if pat and pat.match(hash) is None: + ## raise ValueError("not a valid %s hash" % (cls.name,)) + prefix = cls._hash_prefix + if prefix: + if hash.startswith(prefix): + hash = hash[len(prefix):] + else: + raise exc.InvalidHashError(cls) + return cls(checksum=hash, **context) + + @classmethod + def _norm_hash(cls, hash): + "helper for subclasses to normalize case if needed" + return hash + + def to_string(self): + assert self.checksum is not None + return uascii_to_str(self._hash_prefix + self.checksum) + + @classmethod + def genconfig(cls): + # since it has no settings, there's no need for a config string. + return None + + @classmethod + def genhash(cls, secret, config, **context): + # since it has no settings, just verify config, and call encrypt() + if config is not None and not cls.identify(config): + raise exc.InvalidHashError(cls) + return cls.encrypt(secret, **context) + + # per-subclass: stores dynamically created subclass used by _calc_checksum() stub + __cc_compat_hack = None + + def _calc_checksum(self, secret): + """given secret; calcuate and return encoded checksum portion of hash + string, taking config from object state + """ + # NOTE: prior to 1.6, StaticHandler required classes implement genhash + # instead of this method. so if we reach here, we try calling genhash. + # if that succeeds, we issue deprecation warning. if it fails, + # we'll just recurse back to here, but in a different instance. + # so before we call genhash, we create a subclass which handles + # throwing the NotImplementedError. + cls = self.__class__ + assert cls.__module__ != __name__ + wrapper_cls = cls.__cc_compat_hack + if wrapper_cls is None: + def inner(self, secret): + raise NotImplementedError("%s must implement _calc_checksum()" % + (cls,)) + wrapper_cls = cls.__cc_compat_hack = type(cls.__name__ + "_wrapper", + (cls,), dict(_calc_checksum=inner, __module__=cls.__module__)) + context = dict((k,getattr(self,k)) for k in self.context_kwds) + hash = wrapper_cls.genhash(secret, None, **context) + warn("%r should be updated to implement StaticHandler._calc_checksum() " + "instead of StaticHandler.genhash(), support for the latter " + "style will be removed in Passlib 1.8" % (cls), + DeprecationWarning) + return str_to_uascii(hash) + +#============================================================================= +# GenericHandler mixin classes +#============================================================================= +class HasEncodingContext(GenericHandler): + """helper for classes which require knowledge of the encoding used""" + context_kwds = ("encoding",) + default_encoding = "utf-8" + + def __init__(self, encoding=None, **kwds): + super(HasEncodingContext, self).__init__(**kwds) + self.encoding = encoding or self.default_encoding + +class HasUserContext(GenericHandler): + """helper for classes which require a user context keyword""" + context_kwds = ("user",) + + def __init__(self, user=None, **kwds): + super(HasUserContext, self).__init__(**kwds) + self.user = user + + # XXX: would like to validate user input here, but calls to from_string() + # which lack context keywords would then fail; so leaving code per-handler. + + # wrap funcs to accept 'user' as positional arg for ease of use. + @classmethod + def encrypt(cls, secret, user=None, **context): + return super(HasUserContext, cls).encrypt(secret, user=user, **context) + + @classmethod + def verify(cls, secret, hash, user=None, **context): + return super(HasUserContext, cls).verify(secret, hash, user=user, **context) + + @classmethod + def genhash(cls, secret, config, user=None, **context): + return super(HasUserContext, cls).genhash(secret, config, user=user, **context) + + # XXX: how to guess the entropy of a username? + # most of these hashes are for a system (e.g. Oracle) + # which has a few *very common* names and thus really low entropy; + # while the rest are slightly less predictable. + # need to find good reference about this. + ##@classmethod + ##def bitsize(cls, **kwds): + ## info = super(HasUserContext, cls).bitsize(**kwds) + ## info['user'] = xxx + ## return info + +#------------------------------------------------------------------------ +# checksum mixins +#------------------------------------------------------------------------ class HasRawChecksum(GenericHandler): """mixin for classes which work with decoded checksum bytes @@ -481,50 +798,16 @@ document this class's usage """ + # NOTE: GenericHandler.checksum_chars is ignored by this implementation. - checksum_chars = None - - @classmethod - def norm_checksum(cls, checksum, strict=False): - if checksum is None: - return None - if isinstance(checksum, unicode): - raise TypeError("checksum must be specified as bytes") - cc = cls.checksum_size - if cc and len(checksum) != cc: - raise ValueError("%s checksum must be %d characters" % (cls.name, cc)) - return checksum - -#NOTE: commented out because all use-cases work better with StaticHandler -##class HasNoSettings(GenericHandler): -## """overrides some GenericHandler methods w/ versions more appropriate for hash w/no settings""" -## -## setting_kwds = () -## -## _stub_checksum = None -## -## @classmethod -## def genconfig(cls): -## if cls._stub_checksum: -## return cls().to_string() -## else: -## return None -## -## @classmethod -## def genhash(cls, secret, config): -## if config is None and not cls._stub_checksum: -## self = cls() -## else: -## self = cls.from_string(config) #just to validate the input -## self.checksum = self.calc_checksum(secret) -## return self.to_string() -## -## @classmethod -## def encrypt(cls, secret): -## self = cls() -## self.checksum = self.calc_checksum(secret) -## return self.to_string() - + # NOTE: all HasRawChecksum code is currently part of GenericHandler, + # using private '_checksum_is_bytes' flag. + # this arrangement may be changed in the future. + _checksum_is_bytes = True + +#------------------------------------------------------------------------ +# ident mixins +#------------------------------------------------------------------------ class HasManyIdents(GenericHandler): """mixin for hashes which use multiple prefix identifiers @@ -540,50 +823,50 @@ document this class's usage """ - #========================================================= - #class attrs - #========================================================= - default_ident = None #: should be unicode - ident_values = None #: should be list of unicode strings - ident_aliases = None #: should be dict of unicode -> unicode - #NOTE: any aliases provided to norm_ident() as bytes - # will have been converted to unicode before - # comparing against this dictionary. - - #NOTE: relying on test_06_HasManyIdents() to verify - # these are configured correctly. - - #========================================================= - #instance attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== + default_ident = None # should be unicode + ident_values = None # should be list of unicode strings + ident_aliases = None # should be dict of unicode -> unicode + # NOTE: any aliases provided to norm_ident() as bytes + # will have been converted to unicode before + # comparing against this dictionary. + + # NOTE: relying on test_06_HasManyIdents() to verify + # these are configured correctly. + + #=================================================================== + # instance attrs + #=================================================================== ident = None - #========================================================= - #init - #========================================================= - def __init__(self, ident=None, strict=False, **kwds): - self.ident = self.norm_ident(ident, strict=strict) - super(HasManyIdents, self).__init__(strict=strict, **kwds) - - @classmethod - def norm_ident(cls, ident, strict=False): - #fill in default identifier - if not ident: - if strict: - raise ValueError("no ident specified") - return cls.default_ident + #=================================================================== + # init + #=================================================================== + def __init__(self, ident=None, **kwds): + super(HasManyIdents, self).__init__(**kwds) + self.ident = self._norm_ident(ident) + + def _norm_ident(self, ident): + # fill in default identifier + if ident is None: + if not self.use_defaults: + raise TypeError("no ident specified") + ident = self.default_ident + assert ident is not None, "class must define default_ident" - #handle unicode + # handle unicode if isinstance(ident, bytes): ident = ident.decode('ascii') - #check if identifier is valid - iv = cls.ident_values + # check if identifier is valid + iv = self.ident_values if ident in iv: return ident - #check if it's an alias - ia = cls.ident_aliases + # resolve aliases, and recheck against ident_values + ia = self.ident_aliases if ia: try: value = ia[ident] @@ -593,222 +876,237 @@ if value in iv: return value - #failure! + # failure! raise ValueError("invalid ident: %r" % (ident,)) - #========================================================= - #password hash api - #========================================================= + #=================================================================== + # password hash api + #=================================================================== @classmethod def identify(cls, hash): - if not hash: - return False - if isinstance(hash, bytes): - try: - hash = hash.decode('ascii') - except UnicodeDecodeError: - return False + hash = to_unicode_for_identify(hash) return any(hash.startswith(ident) for ident in cls.ident_values) - #========================================================= - #eoc - #========================================================= - + @classmethod + def _parse_ident(cls, hash): + """extract ident prefix from hash, helper for subclasses' from_string()""" + hash = to_unicode(hash, "ascii", "hash") + for ident in cls.ident_values: + if hash.startswith(ident): + return ident, hash[len(ident):] + raise exc.InvalidHashError(cls) + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# salt mixins +#------------------------------------------------------------------------ class HasSalt(GenericHandler): """mixin for validating salts. This :class:`GenericHandler` mixin adds a ``salt`` keyword to the class constuctor; - any value provided is passed through the :meth:`norm_salt` method, + any value provided is passed through the :meth:`_norm_salt` method, which takes care of validating salt length and content, as well as generating new salts if one it not provided. - :param salt: optional salt string - :param salt_size: optional size of salt (only used if no salt provided); defaults to :attr:`default_salt_size`. - :param strict: if ``True``, requires a valid salt be provided; otherwise is tolerant of correctable errors (the default). + :param salt: + optional salt string + + :param salt_size: + optional size of salt (only used if no salt provided); + defaults to :attr:`default_salt_size`. Class Attributes ================ - In order for :meth:`!norm_salt` to do it's job, the following - attributes must be provided by the handler subclass: + In order for :meth:`!_norm_salt` to do it's job, the following + attributes should be provided by the handler subclass: .. attribute:: min_salt_size - [required] The minimum number of characters allowed in a salt string. - An :exc:`ValueError` will be throw if the salt is too small. + An :exc:`ValueError` will be throw if the provided salt is too small. + Defaults to ``None``, for no minimum. .. attribute:: max_salt_size - [required] The maximum number of characters allowed in a salt string. - When ``strict=True`` (such as when parsing a hash), - an :exc:`ValueError` will be throw if the salt is too large. - WHen ``strict=False`` (such as when parsing user-provided values), - the salt will be silently trimmed to this length if it's too long. + By default an :exc:`ValueError` will be throw if the provided salt is + too large; but if ``relaxed=True``, it will be clipped and a warning + issued instead. Defaults to ``None``, for no maximum. .. attribute:: default_salt_size - [optional] + [required] If no salt is provided, this should specify the size of the salt - that will be generated by :meth:`generate_salt`. - If this is not specified, it will default to :attr:`max_salt_size`. + that will be generated by :meth:`_generate_salt`. By default + this will fall back to :attr:`max_salt_size`. .. attribute:: salt_chars - [required] - A string containing all the characters which are allowed in the salt string. - An :exc:`ValueError` will be throw if any other characters are encountered. - May be set to ``None`` to skip this check (but see in :attr:`default_salt_chars`). + A string containing all the characters which are allowed in the salt + string. An :exc:`ValueError` will be throw if any other characters + are encountered. May be set to ``None`` to skip this check (but see + in :attr:`default_salt_chars`). .. attribute:: default_salt_chars - [optional] + [required] This attribute controls the set of characters use to generate *new* salt strings. By default, it mirrors :attr:`salt_chars`. If :attr:`!salt_chars` is ``None``, this attribute must be specified in order to generate new salts. Aside from that purpose, the main use of this attribute is for hashes which wish to generate - salts from a restricted subset of :attr:`!salt_chars`; such as accepting all characters, - but only using a-z. + salts from a restricted subset of :attr:`!salt_chars`; such as + accepting all characters, but only using a-z. Instance Attributes =================== .. attribute:: salt This instance attribute will be filled in with the salt provided - to the constructor (as adapted by :meth:`norm_salt`) + to the constructor (as adapted by :meth:`_norm_salt`) - Class Methods - ============= - .. automethod:: norm_salt - .. automethod:: generate_salt + Subclassable Methods + ==================== + .. automethod:: _norm_salt + .. automethod:: _generate_salt """ - #TODO: split out "HasRawSalt" mixin for classes where salt should be provided as raw bytes. - # also might need a "HasRawChecksum" to accompany it. - #XXX: allow providing raw salt to this class, and encoding it? - - #========================================================= - #class attrs - #========================================================= - #NOTE: min/max/default_salt_chars is deprecated, use min/max/default_salt_size instead + # TODO: document _truncate_salt() + # XXX: allow providing raw salt to this class, and encoding it? - #: required - minimum size of salt (error if too small) - min_salt_size = None + #=================================================================== + # class attrs + #=================================================================== - #: required - maximum size of salt (truncated if too large) + min_salt_size = None max_salt_size = None + salt_chars = None @classproperty def default_salt_size(cls): - "default salt chars (defaults to max_salt_size if not specified by subclass)" + "default salt size (defaults to *max_salt_size*)" return cls.max_salt_size - #: optional - set of characters allowed in salt string. - salt_chars = None - @classproperty def default_salt_chars(cls): - "required - set of characters used to generate *new* salt strings (defaults to salt_chars)" + "charset used to generate new salt strings (defaults to *salt_chars*)" return cls.salt_chars - #: helper for HasRawSalt, shouldn't be used publically + # private helpers for HasRawSalt, shouldn't be used by subclasses _salt_is_bytes = False - _salt_unit = "char" + _salt_unit = "chars" - #========================================================= - #instance attrs - #========================================================= + #=================================================================== + # instance attrs + #=================================================================== salt = None - #========================================================= - #init - #========================================================= - def __init__(self, salt=None, salt_size=None, strict=False, **kwds): - self.salt = self.norm_salt(salt, salt_size=salt_size, strict=strict) - super(HasSalt, self).__init__(strict=strict, **kwds) + #=================================================================== + # init + #=================================================================== + def __init__(self, salt=None, salt_size=None, **kwds): + super(HasSalt, self).__init__(**kwds) + self.salt = self._norm_salt(salt, salt_size=salt_size) - @classmethod - def generate_salt(cls, salt_size=None, strict=False): - """helper method for norm_salt(); generates a new random salt string. - - :param salt_size: optional salt size, falls back to :attr:`default_salt_size`. - :param strict: if too-large salt should throw error, or merely be trimmed. - """ - if salt_size is None: - salt_size = cls.default_salt_size - else: - mn = cls.min_salt_size - if mn and salt_size < mn: - raise ValueError("%s salt string must be at least %d characters" % (cls.name, mn)) - mx = cls.max_salt_size - if mx and salt_size > mx: - if strict: - raise ValueError("%s salt string must be at most %d characters" % (cls.name, mx)) - salt_size = mx - if cls._salt_is_bytes: - if cls.salt_chars != ALL_BYTE_VALUES: - raise NotImplementedError("raw salts w/ only certain bytes not supported") - return getrandbytes(rng, salt_size) - else: - return getrandstr(rng, cls.default_salt_chars, salt_size) - - @classmethod - def norm_salt(cls, salt, salt_size=None, strict=False): + def _norm_salt(self, salt, salt_size=None): """helper to normalize & validate user-provided salt string + If no salt provided, a random salt is generated + using :attr:`default_salt_size` and :attr:`default_salt_chars`. + :arg salt: salt string or ``None`` - :param strict: enable strict checking (see below); disabled by default + :param salt_size: optionally specified size of autogenerated salt + + :raises TypeError: + If salt not provided and ``use_defaults=False``. :raises ValueError: - * if ``strict=True`` and no salt is provided - * if ``strict=True`` and salt contains greater than :attr:`max_salt_size` characters * if salt contains chars that aren't in :attr:`salt_chars`. * if salt contains less than :attr:`min_salt_size` characters. - - if no salt provided and ``strict=False``, a random salt is generated - using :attr:`default_salt_size` and :attr:`default_salt_chars`. - if the salt is longer than :attr:`max_salt_size` and ``strict=False``, - the salt string is clipped to :attr:`max_salt_size`. + * if ``relaxed=False`` and salt has more than :attr:`max_salt_size` + characters (if ``relaxed=True``, the salt is truncated + and a warning is issued instead). :returns: normalized or generated salt """ - #generate new salt if none provided + # generate new salt if none provided if salt is None: - if strict: - raise ValueError("no salt specified") - return cls.generate_salt(salt_size=salt_size, strict=strict) - - #validate input charset - if cls._salt_is_bytes: - if isinstance(salt, unicode): - raise TypeError("salt must be specified as bytes") + if not self.use_defaults: + raise TypeError("no salt specified") + if salt_size is None: + salt_size = self.default_salt_size + salt = self._generate_salt(salt_size) + + # check type + if self._salt_is_bytes: + if not isinstance(salt, bytes): + raise exc.ExpectedTypeError(salt, "bytes", "salt") else: - if isinstance(salt, bytes): - salt = salt.decode("ascii") - sc = cls.salt_chars - if sc is not None: - for c in salt: - if c not in sc: - raise ValueError("invalid character in %s salt: %r" % (cls.name, c)) + if not isinstance(salt, unicode): + # NOTE: allowing bytes under py2 so salt can be native str. + if isinstance(salt, bytes) and (PY2 or self.relaxed): + salt = salt.decode("ascii") + else: + raise exc.ExpectedTypeError(salt, "unicode", "salt") + + # check charset + sc = self.salt_chars + if sc is not None and any(c not in sc for c in salt): + raise ValueError("invalid characters in %s salt" % self.name) - #check min size - mn = cls.min_salt_size + # check min size + mn = self.min_salt_size if mn and len(salt) < mn: - raise ValueError("%s salt string must be at least %d %ss" % (cls.name, mn, cls._salt_unit)) - - #check max size - mx = cls.max_salt_size - if mx is not None and len(salt) > mx: - if strict: - raise ValueError("%s salt string must be at most %d %ss" % (cls.name, mx, cls._salt_unit)) - salt = salt[:mx] + msg = "salt too small (%s requires %s %d %s)" % (self.name, + "exactly" if mn == self.max_salt_size else ">=", mn, + self._salt_unit) + raise ValueError(msg) + + # check max size + mx = self.max_salt_size + if mx and len(salt) > mx: + msg = "salt too large (%s requires %s %d %s)" % (self.name, + "exactly" if mx == mn else "<=", mx, self._salt_unit) + if self.relaxed: + warn(msg, PasslibHashWarning) + salt = self._truncate_salt(salt, mx) + else: + raise ValueError(msg) return salt - #========================================================= - #eoc - #========================================================= + + @staticmethod + def _truncate_salt(salt, mx): + # NOTE: some hashes (e.g. bcrypt) has structure within their + # salt string. this provides a method to overide to perform + # the truncation properly + return salt[:mx] + + def _generate_salt(self, salt_size): + """helper method for _norm_salt(); generates a new random salt string. + + :arg salt_size: salt size to generate + """ + return getrandstr(rng, self.default_salt_chars, salt_size) + + @classmethod + def bitsize(cls, salt_size=None, **kwds): + "[experimental method] return info about bitsizes of hash" + info = super(HasSalt, cls).bitsize(**kwds) + if salt_size is None: + salt_size = cls.default_salt_size + # FIXME: this may overestimate size due to padding bits + # FIXME: this will be off by 1 for case-insensitive hashes. + info['salt'] = _bitsize(salt_size, cls.default_salt_chars) + return info + + #=================================================================== + # eoc + #=================================================================== class HasRawSalt(HasSalt): """mixin for classes which use decoded salt parameter @@ -822,51 +1120,48 @@ salt_chars = ALL_BYTE_VALUES - #NOTE: all HasRawSalt code is currently part of HasSalt, - # using private _salt_is_bytes flag. - # this arrangement may be changed in the future. + # NOTE: all HasRawSalt code is currently part of HasSalt, using private + # '_salt_is_bytes' flag. this arrangement may be changed in the future. _salt_is_bytes = True - _salt_unit = "byte" + _salt_unit = "bytes" + def _generate_salt(self, salt_size): + assert self.salt_chars in [None, ALL_BYTE_VALUES] + return getrandbytes(rng, salt_size) + +#------------------------------------------------------------------------ +# rounds mixin +#------------------------------------------------------------------------ class HasRounds(GenericHandler): """mixin for validating rounds parameter - This :class:`GenericHandler` mixin adds a ``rounds`` keyword to the class constuctor; - any value provided is passed through the :meth:`norm_rounds` method, - which takes care of validating the number of rounds. + This :class:`GenericHandler` mixin adds a ``rounds`` keyword to the class + constuctor; any value provided is passed through the :meth:`_norm_rounds` + method, which takes care of validating the number of rounds. :param rounds: optional number of rounds hash should use - :param strict: if ``True``, requires a valid rounds vlaue be provided; otherwise is tolerant of correctable errors (the default). Class Attributes ================ - In order for :meth:`!norm_rounds` to do it's job, the following + In order for :meth:`!_norm_rounds` to do it's job, the following attributes must be provided by the handler subclass: .. attribute:: min_rounds - [optional] - The minimum number of rounds allowed. - An :exc:`ValueError` will be thrown if the rounds value is too small. - When ``strict=True`` (such as when parsing a hash), - an :exc:`ValueError` will be throw if the rounds value is too small. - WHen ``strict=False`` (such as when parsing user-provided values), - the rounds value will be silently clipped if it's too small. - Defaults to ``0``. + The minimum number of rounds allowed. A :exc:`ValueError` will be + thrown if the rounds value is too small. Defaults to ``0``. .. attribute:: max_rounds - [required] - The maximum number of rounds allowed. - When ``strict=True`` (such as when parsing a hash), - an :exc:`ValueError` will be throw if the rounds value is too large. - WHen ``strict=False`` (such as when parsing user-provided values), - the rounds value will be silently clipped if it's too large. + The maximum number of rounds allowed. A :exc:`ValueError` will be + thrown if the rounds value is larger than this. Defaults to ``None`` + which indicates no limit to the rounds value. .. attribute:: default_rounds - [required] If no rounds value is provided to constructor, this value will be used. + If this is not specified, a rounds value *must* be specified by the + application. .. attribute:: rounds_cost @@ -876,98 +1171,127 @@ (the default) or ``"log2"``, depending on how the rounds value relates to the actual amount of time that will be required. - .. attribute:: _strict_rounds_bounds - - [optional] - If the handler subclass wishes to *always* throw an error if a rounds - value is provided that's out of bounds (such as when it's provided by the user), - set this private attribute to ``True``. - The default policy in such cases is to silently clip the rounds value - to within :attr:`min_rounds` and :attr:`max_rounds`; - while issuing a :exc:`UserWarning`. - Instance Attributes =================== .. attribute:: rounds This instance attribute will be filled in with the rounds value provided - to the constructor (as adapted by :meth:`norm_rounds`) + to the constructor (as adapted by :meth:`_norm_rounds`) - Class Methods - ============= - .. automethod:: norm_rounds + Subclassable Methods + ==================== + .. automethod:: _norm_rounds """ - #========================================================= - #class attrs - #========================================================= + #=================================================================== + # class attrs + #=================================================================== min_rounds = 0 - max_rounds = None #required by ExtendedHandler.norm_rounds() - default_rounds = None #if not specified, ExtendedHandler.norm_rounds() will require explicit rounds value every time - rounds_cost = "linear" #common case - _strict_rounds_bounds = False #if true, always raises error if specified rounds values out of range - required by spec for some hashes - - #========================================================= - #instance attrs - #========================================================= + max_rounds = None + default_rounds = None + rounds_cost = "linear" # default to the common case + + #=================================================================== + # instance attrs + #=================================================================== rounds = None - #========================================================= - #init - #========================================================= - def __init__(self, rounds=None, strict=False, **kwds): - self.rounds = self.norm_rounds(rounds, strict=strict) - super(HasRounds, self).__init__(strict=strict, **kwds) + #=================================================================== + # init + #=================================================================== + def __init__(self, rounds=None, **kwds): + super(HasRounds, self).__init__(**kwds) + self.rounds = self._norm_rounds(rounds) - @classmethod - def norm_rounds(cls, rounds, strict=False): + def _norm_rounds(self, rounds): """helper routine for normalizing rounds - :arg rounds: rounds integer or ``None`` - :param strict: enable strict checking (see below); disabled by default + :arg rounds: ``None``, or integer cost parameter. + + + :raises TypeError: + * if ``use_defaults=False`` and no rounds is specified + * if rounds is not an integer. :raises ValueError: - * if rounds is ``None`` and ``strict=True`` - * if rounds is ``None`` and no :attr:`default_rounds` are specified by class. - * if rounds is outside bounds of :attr:`min_rounds` and :attr:`max_rounds`, and ``strict=True``. - - if rounds are not specified and ``strict=False``, uses :attr:`default_rounds`. - if rounds are outside bounds and ``strict=False``, rounds are clipped as appropriate, - but a warning is issued. + * if rounds is ``None`` and class does not specify a value for + :attr:`default_rounds`. + * if ``relaxed=False`` and rounds is outside bounds of + :attr:`min_rounds` and :attr:`max_rounds` (if ``relaxed=True``, + the rounds value will be clamped, and a warning issued). :returns: normalized rounds value """ - #provide default if rounds not explicitly set + # fill in default if rounds is None: - if strict: - raise ValueError("no rounds specified") - rounds = cls.default_rounds + if not self.use_defaults: + raise TypeError("no rounds specified") + rounds = self.default_rounds if rounds is None: - raise ValueError("%s rounds value must be specified explicitly" % (cls.name,)) + raise TypeError("%s rounds value must be specified explicitly" + % (self.name,)) - #if class requests, always throw error instead of clipping - if cls._strict_rounds_bounds: - strict = True + # check type + if not isinstance(rounds, int_types): + raise exc.ExpectedTypeError(rounds, "integer", "rounds") - mn = cls.min_rounds + # check bounds + mn = self.min_rounds if rounds < mn: - if strict: - raise ValueError("%s rounds must be >= %d" % (cls.name, mn)) - warn("%s does not allow less than %d rounds: %d" % (cls.name, mn, rounds)) - rounds = mn + msg = "rounds too low (%s requires >= %d rounds)" % (self.name, mn) + if self.relaxed: + warn(msg, PasslibHashWarning) + rounds = mn + else: + raise ValueError(msg) - mx = cls.max_rounds + mx = self.max_rounds if mx and rounds > mx: - if strict: - raise ValueError("%s rounds must be <= %d" % (cls.name, mx)) - warn("%s does not allow more than %d rounds: %d" % (cls.name, mx, rounds)) - rounds = mx + msg = "rounds too high (%s requires <= %d rounds)" % (self.name, mx) + if self.relaxed: + warn(msg, PasslibHashWarning) + rounds = mx + else: + raise ValueError(msg) return rounds - #========================================================= - #eoc - #========================================================= + + @classmethod + def bitsize(cls, rounds=None, vary_rounds=.1, **kwds): + "[experimental method] return info about bitsizes of hash" + info = super(HasRounds, cls).bitsize(**kwds) + # NOTE: this essentially estimates how many bits of "salt" + # can be added by varying the rounds value just a little bit. + if cls.rounds_cost != "log2": + # assume rounds can be randomized within the range + # rounds*(1-vary_rounds) ... rounds*(1+vary_rounds) + # then this can be used to encode + # log2(rounds*(1+vary_rounds)-rounds*(1-vary_rounds)) + # worth of salt-like bits. this works out to + # 1+log2(rounds*vary_rounds) + import math + if rounds is None: + rounds = cls.default_rounds + info['rounds'] = max(0, int(1+math.log(rounds*vary_rounds,2))) + ## else: # log2 rounds + # all bits of the rounds value are critical to choosing + # the time-cost, and can't be randomized. + return info + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# backend mixin & helpers +#------------------------------------------------------------------------ +##def _clear_backend(cls): +## "restore HasManyBackend subclass to unloaded state - used by unittests" +## assert issubclass(cls, HasManyBackends) and cls is not HasManyBackends +## if cls._backend: +## del cls._backend +## del cls._calc_checksum class HasManyBackends(GenericHandler): """GenericHandler mixin which provides selecting from multiple backends. @@ -978,7 +1302,7 @@ For hashes which need to select from multiple backends, depending on the host environment, this class - offers a way to specify alternate :meth:`calc_checksum` methods, + offers a way to specify alternate :meth:`_calc_checksum` methods, and will dynamically chose the best one at runtime. Backend Methods @@ -994,11 +1318,11 @@ which is using :class:`HasManyBackends` as a mixin: .. attribute:: backends - + This attribute should be a tuple containing the names of the backends which are supported. Two common names are ``"os_crypt"`` (if backend uses :mod:`crypt`), and ``"builtin"`` (if the backend is a pure-python - fallback). + fallback). .. attribute:: _has_backend_{name} @@ -1006,23 +1330,24 @@ specific backend is available, it should be either ``True`` or ``False``. One of these should be provided by the subclass for each backend listed in :attr:`backends`. - + .. classmethod:: _calc_checksum_{name} - - private class method that should implement :meth:`calc_checksum` + + private class method that should implement :meth:`_calc_checksum` for a given backend. it will only be called if the backend has been selected by :meth:`set_backend`. One of these should be provided by the subclass for each backend listed in :attr:`backends`. """ - #NOTE: subclass must provide: - # * attr 'backends' containing list of known backends (top priority backend first) - # * attr '_has_backend_xxx' for each backend 'xxx', indicating if backend is available on system - # * attr '_calc_checksum_xxx' for each backend 'xxx', containing calc_checksum implementation using that backend + # NOTE: + # subclass must provide: + # * attr 'backends' containing list of known backends (top priority backend first) + # * attr '_has_backend_xxx' for each backend 'xxx', indicating if backend is available on system + # * attr '_calc_checksum_xxx' for each backend 'xxx', containing calc_checksum implementation using that backend - backends = None #: list of backend names, provided by subclass. + backends = None # list of backend names, provided by subclass. - _backend = None #: holds currently loaded backend (if any) or None + _backend = None # holds currently loaded backend (if any) or None @classmethod def get_backend(cls): @@ -1031,7 +1356,7 @@ if no backend has been loaded, loads and returns name of default backend. - :raises MissingBackendError: if no backends are available. + :raises passlib.exc.MissingBackendError: if no backends are available. :returns: name of active backend """ @@ -1056,17 +1381,11 @@ :returns: ``True`` if backend is currently supported, else ``False``. """ - if name in (None, "any", "default"): - if name is None: - warn("has_backend(None) is deprecated," - " and support will be removed in Passlib 1.6;" - " use has_backend('any') instead.", - DeprecationWarning, stacklevel=2) - try: - cls.set_backend() + if name in ("any", "default"): + if name == "any" and cls._backend: return True - except MissingBackendError: - return False + return any(getattr(cls, "_has_backend_" + name) + for name in cls.backends) elif name in cls.backends: return getattr(cls, "_has_backend_" + name) else: @@ -1078,9 +1397,9 @@ @classmethod def set_backend(cls, name="any"): - """load specified backend to be used for future calc_checksum() calls + """load specified backend to be used for future _calc_checksum() calls - this method replaces :meth:`calc_checksum` with a method + this method replaces :meth:`_calc_checksum` with a method which uses the specified backend. :arg name: @@ -1099,27 +1418,17 @@ the current backend if one has been loaded, else acts like ``"default"``. - :raises MissingBackendError: - * if a specific backend was specified, + :raises passlib.exc.MissingBackendError: + * ... if a specific backend was requested, but is not currently available. - * if ``"any"`` or ``"default"`` was specified, - and NO backends are currently available. - - return value should be ignored. - - .. note:: - - :exc:`~passlib.utils.MissingBackendError` derives - from :exc:`RuntimeError`, since this usually indicates - lack of an external library or OS feature. + * ... if ``"any"`` or ``"default"`` was specified, + and *no* backends are currently available. + + :returns: + + The return value of this function should be ignored. """ - if name is None: - warn("set_backend(None) is deprecated," - " and support will be removed in Passlib 1.6;" - " use set_backend('any') instead.", - DeprecationWarning, stacklevel=2) - name = "any" if name == "any": name = cls._backend if name: @@ -1130,25 +1439,27 @@ if cls.has_backend(name): break else: - raise MissingBackendError(cls._no_backends_msg()) + raise exc.MissingBackendError(cls._no_backends_msg()) elif not cls.has_backend(name): - raise MissingBackendError("%s backend not available: %r" % (cls.name, name)) - cls.calc_checksum = getattr(cls, "_calc_checksum_" + name) + raise exc.MissingBackendError("%s backend not available: %r" % + (cls.name, name)) + cls._calc_checksum = getattr(cls, "_calc_checksum_" + name) cls._backend = name return name - def calc_checksum(self, secret): - "stub for calc_checksum(), default backend will be selected first time stub is called" - #backend not loaded - run detection and call replacement + def _calc_checksum(self, secret): + "stub for _calc_checksum(), default backend will be selected first time stub is called" + # if we got here, no backend has been loaded; so load default backend assert not self._backend, "set_backend() failed to replace lazy loader" self.set_backend() assert self._backend, "set_backend() failed to load a default backend" - #set_backend() should have replaced this method, so call it again. - return self.calc_checksum(secret) -#========================================================= -#wrappers -#========================================================= + # this should now invoke the backend-specific version, so call it again. + return self._calc_checksum(secret) + +#============================================================================= +# wrappers +#============================================================================= class PrefixWrapper(object): """wraps another handler, adding a constant prefix. @@ -1170,7 +1481,8 @@ :param lazy: if True and wrapped handler is specified by name, don't look it up until needed. """ - def __init__(self, name, wrapped, prefix=u'', orig_prefix=u'', lazy=False, doc=None): + def __init__(self, name, wrapped, prefix=u(''), orig_prefix=u(''), lazy=False, + doc=None, ident=None): self.name = name if isinstance(prefix, bytes): prefix = prefix.decode("ascii") @@ -1188,13 +1500,29 @@ if not lazy: self._get_wrapped() + if ident is not None: + if ident is True: + # signal that prefix is identifiable in itself. + if prefix: + ident = prefix + else: + raise ValueError("no prefix specified") + if isinstance(ident, bytes): + ident = ident.decode("ascii") + # XXX: what if ident includes parts of wrapped hash's ident? + if ident[:len(prefix)] != prefix[:len(ident)]: + raise ValueError("ident must agree with prefix") + self._ident = ident + _wrapped_name = None _wrapped_handler = None def _check_handler(self, handler): if 'ident' in handler.setting_kwds and self.orig_prefix: - #TODO: look into way to fix the issues. - warn("PrefixWrapper: 'orig_prefix' option may not work correctly for handlers which have multiple identifiers: %r" % (handler.name,)) + # TODO: look into way to fix the issues. + warn("PrefixWrapper: 'orig_prefix' option may not work correctly " + "for handlers which have multiple identifiers: %r" % + (handler.name,), exc.PasslibRuntimeWarning) def _get_wrapped(self): handler = self._wrapped_handler @@ -1206,14 +1534,50 @@ wrapped = property(_get_wrapped) - ##@property - ##def ident(self): - ## return self._prefix + _ident = False + + @property + def ident(self): + value = self._ident + if value is False: + value = None + # XXX: how will this interact with orig_prefix ? + # not exposing attrs for now if orig_prefix is set. + if not self.orig_prefix: + wrapped = self.wrapped + ident = getattr(wrapped, "ident", None) + if ident is not None: + value = self._wrap_hash(ident) + self._ident = value + return value + + _ident_values = False + + @property + def ident_values(self): + value = self._ident_values + if value is False: + value = None + # XXX: how will this interact with orig_prefix ? + # not exposing attrs for now if orig_prefix is set. + if not self.orig_prefix: + wrapped = self.wrapped + idents = getattr(wrapped, "ident_values", None) + if idents: + value = [ self._wrap_hash(ident) for ident in idents ] + ##else: + ## ident = self.ident + ## if ident is not None: + ## value = [ident] + self._ident_values = value + return value - #attrs that should be proxied + # attrs that should be proxied _proxy_attrs = ( "setting_kwds", "context_kwds", "default_rounds", "min_rounds", "max_rounds", "rounds_cost", + "default_salt_size", "min_salt_size", "max_salt_size", + "salt_chars", "default_salt_chars", "backends", "has_backend", "get_backend", "set_backend", ) @@ -1222,43 +1586,49 @@ if self.prefix: args.append("prefix=%r" % self.prefix) if self.orig_prefix: - args.append("orig_prefix=%r", self.orig_prefix) + args.append("orig_prefix=%r" % self.orig_prefix) args = ", ".join(args) return 'PrefixWrapper(%r, %s)' % (self.name, args) + def __dir__(self): + attrs = set(dir(self.__class__)) + attrs.update(self.__dict__) + wrapped = self.wrapped + attrs.update( + attr for attr in self._proxy_attrs + if hasattr(wrapped, attr) + ) + return list(attrs) + def __getattr__(self, attr): - "proxy most attributes from wrapped class (eg rounds, salt size, etc)" + "proxy most attributes from wrapped class (e.g. rounds, salt size, etc)" if attr in self._proxy_attrs: return getattr(self.wrapped, attr) raise AttributeError("missing attribute: %r" % (attr,)) def _unwrap_hash(self, hash): "given hash belonging to wrapper, return orig version" - if isinstance(hash, bytes): - hash = hash.decode('ascii') + # NOTE: assumes hash has been validated as unicode already prefix = self.prefix if not hash.startswith(prefix): - raise ValueError("not a valid %s hash" % (self.name,)) - #NOTE: always passing to handler as unicode, to save reconversion + raise exc.InvalidHashError(self) + # NOTE: always passing to handler as unicode, to save reconversion return self.orig_prefix + hash[len(prefix):] def _wrap_hash(self, hash): "given orig hash; return one belonging to wrapper" - #NOTE: should usually be native string. + # NOTE: should usually be native string. # (which does mean extra work under py2, but not py3) if isinstance(hash, bytes): - hash = hash.decode('ascii') + hash = hash.decode("ascii") orig_prefix = self.orig_prefix if not hash.startswith(orig_prefix): - raise ValueError("not a valid %s hash" % (self.wrapped.name,)) + raise exc.InvalidHashError(self.wrapped) wrapped = self.prefix + hash[len(orig_prefix):] - return to_hash_str(wrapped) + return uascii_to_str(wrapped) def identify(self, hash): - if not hash: - return False - if isinstance(hash, bytes): - hash = hash.decode('ascii') + hash = to_unicode_for_identify(hash) if not hash.startswith(self.prefix): return False hash = self._unwrap_hash(hash) @@ -1266,13 +1636,14 @@ def genconfig(self, **kwds): config = self.wrapped.genconfig(**kwds) - if config: - return self._wrap_hash(config) + if config is None: + return None else: - return config + return self._wrap_hash(config) def genhash(self, secret, config, **kwds): - if config: + if config is not None: + config = to_unicode(config, "ascii", "config/hash") config = self._unwrap_hash(config) return self._wrap_hash(self.wrapped.genhash(secret, config, **kwds)) @@ -1280,11 +1651,10 @@ return self._wrap_hash(self.wrapped.encrypt(secret, **kwds)) def verify(self, secret, hash, **kwds): - if not hash: - raise ValueError("no %s hash specified" % (self.name,)) + hash = to_unicode(hash, "ascii", "hash") hash = self._unwrap_hash(hash) return self.wrapped.verify(secret, hash, **kwds) -#========================================================= +#============================================================================= # eof -#========================================================= +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/md4.py passlib-1.6.1/passlib/utils/md4.py --- passlib-1.5.3/passlib/utils/md4.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/utils/md4.py 2012-08-01 17:14:10.000000000 +0000 @@ -7,20 +7,20 @@ """ -#========================================================================= -#imports -#========================================================================= -#core +#============================================================================= +# imports +#============================================================================= +# core from binascii import hexlify import struct from warnings import warn -#site -from passlib.utils import b, bytes, to_native_str -#local +# site +from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY3 +# local __all__ = [ "md4" ] -#========================================================================= -#utils -#========================================================================= +#============================================================================= +# utils +#============================================================================= def F(x,y,z): return (x&y) | ((~x) & z) @@ -32,9 +32,9 @@ MASK_32 = 2**32-1 -#========================================================================= -#main class -#========================================================================= +#============================================================================= +# main class +#============================================================================= class md4(object): """pep-247 compatible implementation of MD4 hash algorithm @@ -58,16 +58,16 @@ return hexdecimal version of digest """ - #FIXME: make this follow hash object PEP better. - #FIXME: this isn't threadsafe - #XXX: should we monkeypatch ourselves into hashlib for general use? probably wouldn't be nice. + # FIXME: make this follow hash object PEP better. + # FIXME: this isn't threadsafe + # XXX: should we monkeypatch ourselves into hashlib for general use? probably wouldn't be nice. name = "md4" digest_size = digestsize = 16 - _count = 0 #number of 64-byte blocks processed so far (not including _buf) - _state = None #list of [a,b,c,d] 32 bit ints used as internal register - _buf = None #data processed in 64 byte blocks, this holds leftover from last update + _count = 0 # number of 64-byte blocks processed so far (not including _buf) + _state = None # list of [a,b,c,d] 32 bit ints used as internal register + _buf = None # data processed in 64 byte blocks, this holds leftover from last update def __init__(self, content=None): self._count = 0 @@ -76,7 +76,7 @@ if content: self.update(content) - #round 1 table - [abcd k s] + # round 1 table - [abcd k s] _round1 = [ [0,1,2,3, 0,3], [3,0,1,2, 1,7], @@ -99,7 +99,7 @@ [1,2,3,0, 15,19], ] - #round 2 table - [abcd k s] + # round 2 table - [abcd k s] _round2 = [ [0,1,2,3, 0,3], [3,0,1,2, 4,5], @@ -122,7 +122,7 @@ [1,2,3,0, 15,13], ] - #round 3 table - [abcd k s] + # round 3 table - [abcd k s] _round3 = [ [0,1,2,3, 0,3], [3,0,1,2, 8,9], @@ -147,30 +147,30 @@ def _process(self, block): "process 64 byte block" - #unpack block into 16 32-bit ints + # unpack block into 16 32-bit ints X = struct.unpack("<16I", block) - #clone state + # clone state orig = self._state state = list(orig) - #round 1 - F function - (x&y)|(~x & z) + # round 1 - F function - (x&y)|(~x & z) for a,b,c,d,k,s in self._round1: t = (state[a] + F(state[b],state[c],state[d]) + X[k]) & MASK_32 state[a] = ((t<>(32-s)) - #round 2 - G function + # round 2 - G function for a,b,c,d,k,s in self._round2: t = (state[a] + G(state[b],state[c],state[d]) + X[k] + 0x5a827999) & MASK_32 state[a] = ((t<>(32-s)) - #round 3 - H function - x ^ y ^ z + # round 3 - H function - x ^ y ^ z for a,b,c,d,k,s in self._round3: t = (state[a] + (state[b] ^ state[c] ^ state[d]) + X[k] + 0x6ed9eba1) & MASK_32 state[a] = ((t<>(32-s)) - #add back into original state - for i in xrange(4): + # add back into original state + for i in irange(4): orig[i] = (orig[i]+state[i]) & MASK_32 def update(self, content): @@ -199,11 +199,11 @@ return other def digest(self): - #NOTE: backing up state so we can restore it after _process is called, - #in case object is updated again (this is only attr altered by this method) + # NOTE: backing up state so we can restore it after _process is called, + # in case object is updated again (this is only attr altered by this method) orig = list(self._state) - #final block: buf + 0x80, + # final block: buf + 0x80, # then 0x00 padding until congruent w/ 56 mod 64 bytes # then last 8 bytes = msg length in bits buf = self._buf @@ -217,52 +217,50 @@ assert len(block) == 64 self._process(block) - #render digest & restore un-finalized state + # render digest & restore un-finalized state out = struct.pack("<4I", *self._state) self._state = orig return out def hexdigest(self): - return to_native_str(hexlify(self.digest()), "latin-1") + return bascii_to_str(hexlify(self.digest())) - #========================================================================= - #eoc - #========================================================================= + #=================================================================== + # eoc + #=================================================================== -#keep ref around for unittest, 'md4' usually replaced by ssl wrapper, below. +# keep ref around for unittest, 'md4' usually replaced by ssl wrapper, below. _builtin_md4 = md4 -#========================================================================= -#check if hashlib provides accelarated md4 -#========================================================================= -from passlib.utils import pypy_vm +#============================================================================= +# check if hashlib provides accelarated md4 +#============================================================================= import hashlib +from passlib.utils import PYPY -def _has_native_md4(): +def _has_native_md4(): # pragma: no cover -- runtime detection try: h = hashlib.new("md4") except ValueError: - #not supported + # not supported - ssl probably missing (e.g. ironpython) return False result = h.hexdigest() if result == '31d6cfe0d16ae931b73c59d7e0c089c0': return True - if pypy_vm and result == '': - #as of 1.5, pypy md4 just returns null! - #since this is expected, don't bother w/ warning. + if PYPY and result == '': + # workaround for https://bugs.pypy.org/issue957, fixed in PyPy 1.8 return False - #anything else should alert user - warn("native md4 support disabled, incorrect value returned") + # anything else and we should alert user + from passlib.exc import PasslibRuntimeWarning + warn("native md4 support disabled, sanity check failed!", PasslibRuntimeWarning) return False -if _has_native_md4(): - #overwrite md4 class w/ hashlib wrapper +if _has_native_md4(): + # overwrite md4 class w/ hashlib wrapper def md4(content=None): "wrapper for hashlib.new('md4')" return hashlib.new('md4', content or b('')) -else: - del hashlib -#========================================================================= -#eof -#========================================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/utils/pbkdf2.py passlib-1.6.1/passlib/utils/pbkdf2.py --- passlib-1.5.3/passlib/utils/pbkdf2.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/utils/pbkdf2.py 2012-08-01 17:14:10.000000000 +0000 @@ -3,90 +3,199 @@ this module is getting increasingly poorly named. maybe rename to "kdf" since it's getting more key derivation functions added. """ -#================================================================================= -#imports -#================================================================================= -#core -from binascii import unhexlify +#============================================================================= +# imports +#============================================================================= +# core import hashlib -import hmac import logging; log = logging.getLogger(__name__) import re from struct import pack from warnings import warn -#site +# site try: from M2Crypto import EVP as _EVP except ImportError: _EVP = None -#pkg -from passlib.utils import xor_bytes, to_bytes, native_str, b, bytes -#local +# pkg +from passlib.exc import PasslibRuntimeWarning, ExpectedTypeError +from passlib.utils import join_bytes, to_native_str, bytes_to_int, int_to_bytes, join_byte_values +from passlib.utils.compat import b, bytes, BytesIO, irange, callable, int_types +# local __all__ = [ - "hmac_sha1", "get_prf", "pbkdf1", "pbkdf2", ] -# Py2k # -from cStringIO import StringIO as BytesIO -# Py3k # -#from io import BytesIO -# end Py3k # - -#================================================================================= -#quick hmac_sha1 implementation used various places -#================================================================================= -def hmac_sha1(key, msg): - "perform raw hmac-sha1 of a message" - return hmac.new(key, msg, hashlib.sha1).digest() +#============================================================================= +# hash helpers +#============================================================================= + +# known hash names +_nhn_formats = dict(hashlib=0, iana=1) +_nhn_hash_names = [ + # (hashlib/ssl name, iana name or standin, ... other known aliases) + + # hashes with official IANA-assigned names + # (as of 2012-03 - http://www.iana.org/assignments/hash-function-text-names) + ("md2", "md2"), + ("md5", "md5"), + ("sha1", "sha-1"), + ("sha224", "sha-224", "sha2-224"), + ("sha256", "sha-256", "sha2-256"), + ("sha384", "sha-384", "sha2-384"), + ("sha512", "sha-512", "sha2-512"), + + # hashlib/ssl-supported hashes without official IANA names, + # hopefully compatible stand-ins have been chosen. + ("md4", "md4"), + ("sha", "sha-0", "sha0"), + ("ripemd", "ripemd"), + ("ripemd160", "ripemd-160"), +] + +# cache for norm_hash_name() +_nhn_cache = {} + +def norm_hash_name(name, format="hashlib"): + """Normalize hash function name + + :arg name: + Original hash function name. -if _EVP: - #default *should* be sha1, which saves us a wrapper function, but might as well check. + This name can be a Python :mod:`~hashlib` digest name, + a SCRAM mechanism name, IANA assigned hash name, etc. + Case is ignored, and underscores are converted to hyphens. + + :param format: + Naming convention to normalize to. + Possible values are: + + * ``"hashlib"`` (the default) - normalizes name to be compatible + with Python's :mod:`!hashlib`. + + * ``"iana"`` - normalizes name to IANA-assigned hash function name. + for hashes which IANA hasn't assigned a name for, issues a warning, + and then uses a heuristic to give a "best guess". + + :returns: + Hash name, returned as native :class:`!str`. + """ + # check cache try: - result = _EVP.hmac(b('x'),b('y')) - except ValueError: #pragma: no cover - #this is probably not a good sign if it happens. - warn("PassLib: M2Crypt.EVP.hmac() unexpected threw value error during passlib startup test") - else: - if result == b(',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i'): - hmac_sha1 = _EVP.hmac + idx = _nhn_formats[format] + except KeyError: + raise ValueError("unknown format: %r" % (format,)) + try: + return _nhn_cache[name][idx] + except KeyError: + pass + orig = name + + # normalize input + if not isinstance(name, str): + name = to_native_str(name, 'utf-8', 'hash name') + name = re.sub("[_ /]", "-", name.strip().lower()) + if name.startswith("scram-"): + name = name[6:] + if name.endswith("-plus"): + name = name[:-5] + + # look through standard names and known aliases + def check_table(name): + for row in _nhn_hash_names: + if name in row: + _nhn_cache[orig] = row + return row[idx] + result = check_table(name) + if result: + return result + + # try to clean name up, and recheck table + m = re.match("^(?P[a-z]+)-?(?P\d)?-?(?P\d{3,4})?$", name) + if m: + name, rev, size = m.group("name", "rev", "size") + if rev: + name += rev + if size: + name += "-" + size + result = check_table(name) + if result: + return result + + # else we've done what we can + warn("norm_hash_name(): unknown hash: %r" % (orig,), PasslibRuntimeWarning) + name2 = name.replace("-", "") + row = _nhn_cache[orig] = (name2, name) + return row[idx] + +# TODO: get_hash() func which wraps norm_hash_name(), hashlib., and hashlib.new + +#============================================================================= +# general prf lookup +#============================================================================= +_BNULL = b('\x00') +_XY_DIGEST = b(',\x1cb\xe0H\xa5\x82M\xfb>\xd6\x98\xef\x8e\xf9oQ\x85\xa3i') + +_trans_5C = join_byte_values((x ^ 0x5C) for x in irange(256)) +_trans_36 = join_byte_values((x ^ 0x36) for x in irange(256)) -#================================================================================= -#general prf lookup -#================================================================================= def _get_hmac_prf(digest): "helper to return HMAC prf for specific digest" - #check if m2crypto is present and supports requested digest - if _EVP: + def tag_wrapper(prf): + prf.__name__ = "hmac_" + digest + prf.__doc__ = ("hmac_%s(key, msg) -> digest;" + " generated by passlib.utils.pbkdf2.get_prf()" % + digest) + + if _EVP and digest == "sha1": + # use m2crypto function directly for sha1, since that's it's default digest + try: + result = _EVP.hmac(b('x'),b('y')) + except ValueError: # pragma: no cover + pass + else: + if result == _XY_DIGEST: + return _EVP.hmac, 20 + # don't expect to ever get here, but will fall back to pure-python if we do. + warn("M2Crypto.EVP.HMAC() returned unexpected result " # pragma: no cover -- sanity check + "during Passlib self-test!", PasslibRuntimeWarning) + elif _EVP: + # use m2crypto if it's present and supports requested digest try: result = _EVP.hmac(b('x'), b('y'), digest) except ValueError: pass else: - #it does. so use M2Crypto's hmac & digest code + # it does. so use M2Crypto's hmac & digest code hmac_const = _EVP.hmac def prf(key, msg): - "prf(key,msg)->digest; generated by passlib.utils.pbkdf2.get_prf()" return hmac_const(key, msg, digest) - prf.__name__ = "hmac_" + digest digest_size = len(result) + tag_wrapper(prf) return prf, digest_size - #fall back to stdlib implementation + # fall back to hashlib-based implementation digest_const = getattr(hashlib, digest, None) if not digest_const: raise ValueError("unknown hash algorithm: %r" % (digest,)) - digest_size = digest_const().digest_size - hmac_const = hmac.new + tmp = digest_const() + block_size = tmp.block_size + assert block_size >= 16, "unacceptably low block size" + digest_size = tmp.digest_size + del tmp def prf(key, msg): - "prf(key,msg)->digest; generated by passlib.utils.pbkdf2.get_prf()" - return hmac_const(key, msg, digest_const).digest() - prf.__name__ = "hmac_" + digest + # simplified version of stdlib's hmac module + if len(key) > block_size: + key = digest_const(key).digest() + key += _BNULL * (block_size - len(key)) + tmp = digest_const(key.translate(_trans_36) + msg).digest() + return digest_const(key.translate(_trans_5C) + tmp).digest() + tag_wrapper(prf) return prf, digest_size -#cache mapping prf name/func -> (func, digest_size) +# cache mapping prf name/func -> (func, digest_size) _prf_cache = {} def _clear_prf_cache(): @@ -137,156 +246,170 @@ global _prf_cache if name in _prf_cache: return _prf_cache[name] - if isinstance(name, native_str): + if isinstance(name, str): if name.startswith("hmac-") or name.startswith("hmac_"): retval = _get_hmac_prf(name[5:]) else: raise ValueError("unknown prf algorithm: %r" % (name,)) elif callable(name): - #assume it's a callable, use it directly + # assume it's a callable, use it directly digest_size = len(name(b('x'),b('y'))) retval = (name, digest_size) else: - raise TypeError("prf must be string or callable") + raise ExpectedTypeError(name, "str or callable", "prf name") _prf_cache[name] = retval return retval -#================================================================================= -#pbkdf1 support -#================================================================================= -def pbkdf1(secret, salt, rounds, keylen, hash="sha1"): +#============================================================================= +# pbkdf1 support +#============================================================================= +def pbkdf1(secret, salt, rounds, keylen=None, hash="sha1"): """pkcs#5 password-based key derivation v1.5 :arg secret: passphrase to use to generate key :arg salt: salt string to use when generating key :param rounds: number of rounds to use to generate key - :arg keylen: number of bytes to generate + :arg keylen: number of bytes to generate (if ``None``, uses digest's native size) :param hash: - hash function to use. - if specified, it must be one of the following: - - * a callable with the prototype ``hash(message) -> raw digest`` - * a string matching one of the hashes recognized by hashlib + hash function to use. must be name of a hash recognized by hashlib. :returns: raw bytes of generated key .. note:: - This algorithm is deprecated, new code should use PBKDF2. - Among other reasons, ``keylen`` cannot be larger + This algorithm has been deprecated, new code should use PBKDF2. + Among other limitations, ``keylen`` cannot be larger than the digest size of the specified hash. """ - #prepare secret & salt + # validate secret & salt if not isinstance(secret, bytes): - raise TypeError("secret must be bytes, not %s" % (type(secret),)) + raise ExpectedTypeError(secret, "bytes", "secret") if not isinstance(salt, bytes): - raise TypeError("salt must be bytes, not %s" % (type(salt),)) + raise ExpectedTypeError(salt, "bytes", "salt") - #prepare rounds - if not isinstance(rounds, (int, long)): - raise TypeError("rounds must be an integer") + # validate rounds + if not isinstance(rounds, int_types): + raise ExpectedTypeError(rounds, "int", "rounds") if rounds < 1: raise ValueError("rounds must be at least 1") - #prep keylen - if keylen < 0: + # resolve hash + try: + hash_const = getattr(hashlib, hash) + except AttributeError: + # check for ssl hash + # NOTE: if hash unknown, new() will throw ValueError, which we'd just + # reraise anyways; so instead of checking, we just let it get + # thrown during first use, below + # TODO: use builtin md4 class if hashlib doesn't have it. + def hash_const(msg): + return hashlib.new(hash, msg) + + # prime pbkdf1 loop, get block size + block = hash_const(secret + salt).digest() + + # validate keylen + if keylen is None: + keylen = len(block) + elif not isinstance(keylen, int_types): + raise ExpectedTypeError(keylen, "int or None", "keylen") + elif keylen < 0: raise ValueError("keylen must be at least 0") - - #resolve hash - if isinstance(hash, native_str): - #check for builtin hash - hf = getattr(hashlib, hash, None) - if hf is None: - #check for ssl hash - #NOTE: if hash unknown, will throw ValueError, which we'd just - # reraise anyways; so instead of checking, we just let it get - # thrown during first use, below - def hf(msg): - return hashlib.new(hash, msg) - - #run pbkdf1 - block = hf(secret + salt).digest() - if keylen > len(block): - raise ValueError, "keylength too large for digest: %r > %r" % (keylen, len(block)) - r = 1 - while r < rounds: - block = hf(block).digest() - r += 1 + elif keylen > len(block): + raise ValueError("keylength too large for digest: %r > %r" % + (keylen, len(block))) + + # main pbkdf1 loop + for _ in irange(rounds-1): + block = hash_const(block).digest() return block[:keylen] -#================================================================================= -#pbkdf2 -#================================================================================= -MAX_BLOCKS = 0xffffffff #2**32-1 +#============================================================================= +# pbkdf2 +#============================================================================= +MAX_BLOCKS = 0xffffffff # 2**32-1 MAX_HMAC_SHA1_KEYLEN = MAX_BLOCKS*20 +# NOTE: the pbkdf2 spec does not specify a maximum number of rounds. +# however, many of the hashes in passlib are currently clamped +# at the 32-bit limit, just for sanity. once realistic pbkdf2 rounds +# start approaching 24 bits, this limit will be raised. -def pbkdf2(secret, salt, rounds, keylen, prf="hmac-sha1"): +def pbkdf2(secret, salt, rounds, keylen=None, prf="hmac-sha1"): """pkcs#5 password-based key derivation v2.0 :arg secret: passphrase to use to generate key :arg salt: salt string to use when generating key :param rounds: number of rounds to use to generate key - :arg keylen: number of bytes to generate + :arg keylen: + number of bytes to generate. + if set to ``None``, will use digest size of selected prf. :param prf: psuedo-random family to use for key strengthening. this can be any string or callable accepted by :func:`get_prf`. - this defaults to ``hmac-sha1`` (the only prf explicitly listed in + this defaults to ``"hmac-sha1"`` (the only prf explicitly listed in the PBKDF2 specification) :returns: raw bytes of generated key """ - #prepare secret & salt + # validate secret & salt if not isinstance(secret, bytes): - raise TypeError("secret must be bytes, not %s" % (type(secret),)) + raise ExpectedTypeError(secret, "bytes", "secret") if not isinstance(salt, bytes): - raise TypeError("salt must be bytes, not %s" % (type(salt),)) + raise ExpectedTypeError(salt, "bytes", "salt") - #prepare rounds - if not isinstance(rounds, (int, long)): - raise TypeError("rounds must be an integer") + # validate rounds + if not isinstance(rounds, int_types): + raise ExpectedTypeError(rounds, "int", "rounds") if rounds < 1: raise ValueError("rounds must be at least 1") - #special case for m2crypto + hmac-sha1 + # validate keylen + if keylen is not None: + if not isinstance(keylen, int_types): + raise ExpectedTypeError(keylen, "int or None", "keylen") + elif keylen < 0: + raise ValueError("keylen must be at least 0") + + # special case for m2crypto + hmac-sha1 if prf == "hmac-sha1" and _EVP: - #NOTE: doing check here, because M2crypto won't take longs (which this is, under 32bit) + if keylen is None: + keylen = 20 + # NOTE: doing check here, because M2crypto won't take 'long' instances + # (which this is when running under 32bit) if keylen > MAX_HMAC_SHA1_KEYLEN: - raise ValueError("key length too long") + raise ValueError("key length too long for digest") - #NOTE: M2crypto reliably segfaults for me if given keylengths - # larger than 40 (crashes at 41 on one system, 61 on another). - # so just avoiding it for longer calls. - if keylen < 41: + # NOTE: as of 2012-4-4, m2crypto has buffer overflow issue + # which may cause segfaults if keylen > 32 (EVP_MAX_KEY_LENGTH). + # therefore we're avoiding m2crypto for large keys until that's fixed. + # see https://bugzilla.osafoundation.org/show_bug.cgi?id=13052 + if keylen < 32: return _EVP.pbkdf2(secret, salt, rounds, keylen) - #resolve prf - encode_block, digest_size = get_prf(prf) - - #figure out how many blocks we'll need - bcount = (keylen+digest_size-1)//digest_size - if bcount >= MAX_BLOCKS: - raise ValueError("key length to long") - - #build up key from blocks - out = BytesIO() - write = out.write - for i in xrange(1,bcount+1): - block = tmp = encode_block(secret, salt + pack(">L", i)) - #NOTE: could potentially unroll this loop somewhat for speed, - # or find some faster way to accumulate & xor tmp values together - j = 1 - while j < rounds: - tmp = encode_block(secret, tmp) - block = xor_bytes(block, tmp) - j += 1 - write(block) - - #and done - return out.getvalue()[:keylen] - -#================================================================================= -#eof -#================================================================================= + # resolve prf + prf_func, digest_size = get_prf(prf) + if keylen is None: + keylen = digest_size + + # figure out how many blocks we'll need + block_count = (keylen+digest_size-1)//digest_size + if block_count >= MAX_BLOCKS: + raise ValueError("key length too long for digest") + + # build up result from blocks + def gen(): + for i in irange(block_count): + digest = prf_func(secret, salt + pack(">L", i+1)) + accum = bytes_to_int(digest) + for _ in irange(rounds-1): + digest = prf_func(secret, digest) + accum ^= bytes_to_int(digest) + yield int_to_bytes(accum, digest_size) + return join_bytes(gen())[:keylen] + +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/passlib/win32.py passlib-1.6.1/passlib/win32.py --- passlib-1.5.3/passlib/win32.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/passlib/win32.py 2012-08-01 17:14:10.000000000 +0000 @@ -1,4 +1,4 @@ -"""passlib.win32 - MS Windows support +"""passlib.win32 - MS Windows support - DEPRECATED, WILL BE REMOVED IN 1.8 the LMHASH and NTHASH algorithms are used in various windows related contexts, but generally not in a manner compatible with how passlib is structured. @@ -20,42 +20,49 @@ See also :mod:`passlib.hash.nthash`. """ -#========================================================= -#imports -#========================================================= -#core + +from warnings import warn +warn("the 'passlib.win32' module is deprecated, and will be removed in " + "passlib 1.8; please use the 'passlib.hash.nthash' and " + "'passlib.hash.lmhash' classes instead.", + DeprecationWarning) + +#============================================================================= +# imports +#============================================================================= +# core from binascii import hexlify -#site -#pkg -from passlib.utils import to_native_str, b +# site +# pkg +from passlib.utils.compat import b, unicode from passlib.utils.des import des_encrypt_block from passlib.hash import nthash -#local +# local __all__ = [ "nthash", "raw_lmhash", "raw_nthash", ] -#========================================================= -#helpers -#========================================================= +#============================================================================= +# helpers +#============================================================================= LM_MAGIC = b("KGS!@#$%") raw_nthash = nthash.raw_nthash def raw_lmhash(secret, encoding="ascii", hex=False): "encode password using des-based LMHASH algorithm; returns string of raw bytes, or unicode hex" - #NOTE: various references say LMHASH uses the OEM codepage of the host - # for it's encoding. until a clear reference is found, - # as well as a path for getting the encoding, - # letting this default to "ascii" to prevent incorrect hashes - # from being made w/o user explicitly choosing an encoding. + # NOTE: various references say LMHASH uses the OEM codepage of the host + # for it's encoding. until a clear reference is found, + # as well as a path for getting the encoding, + # letting this default to "ascii" to prevent incorrect hashes + # from being made w/o user explicitly choosing an encoding. if isinstance(secret, unicode): secret = secret.encode(encoding) ns = secret.upper()[:14] + b("\x00") * (14-len(secret)) out = des_encrypt_block(ns[:7], LM_MAGIC) + des_encrypt_block(ns[7:], LM_MAGIC) return hexlify(out).decode("ascii") if hex else out -#========================================================= -#eoc -#========================================================= +#============================================================================= +# eoc +#============================================================================= diff -Nru passlib-1.5.3/passlib.egg-info/PKG-INFO passlib-1.6.1/passlib.egg-info/PKG-INFO --- passlib-1.5.3/passlib.egg-info/PKG-INFO 2011-10-08 04:58:49.000000000 +0000 +++ passlib-1.6.1/passlib.egg-info/PKG-INFO 2012-08-02 19:28:07.000000000 +0000 @@ -1,27 +1,26 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: passlib -Version: 1.5.3 -Summary: comprehensive password hashing framework supporting over 20 schemes +Version: 1.6.1 +Summary: comprehensive password hashing framework supporting over 30 schemes Home-page: http://passlib.googlecode.com Author: Eli Collins Author-email: elic@assurancetechnologies.com License: BSD -Download-URL: http://passlib.googlecode.com/files/passlib-1.5.3.tar.gz -Description: Passlib is a password hashing library for Python 2 & 3, - which provides cross-platform implementations of over 20 - password hashing algorithms, as well as a framework for - managing existing password hashes. It's designed to be useful - for a wide range of tasks, from verifying a hash found in /etc/shadow, - to providing full-strength password hashing for multi-user application. +Download-URL: http://passlib.googlecode.com/files/passlib-1.6.1.tar.gz +Description: Passlib is a password hashing library for Python 2 & 3, which provides + cross-platform implementations of over 30 password hashing algorithms, as well + as a framework for managing existing password hashes. It's designed to be useful + for a wide range of tasks, from verifying a hash found in /etc/shadow, to + providing full-strength password hashing for multi-user application. * See the `online documentation `_ for details, installation instructions, and examples. - * See the `passlib homepage `_ + * See the `Passlib homepage `_ for the latest news, more information, and additional downloads. * See the `changelog `_ - for description of what's new in Passlib. + for a description of what's new in Passlib. All releases are signed with the gpg key `4CE1ED31 `_. @@ -36,6 +35,9 @@ Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: Jython +Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Libraries Classifier: Development Status :: 5 - Production/Stable diff -Nru passlib-1.5.3/passlib.egg-info/SOURCES.txt passlib-1.6.1/passlib.egg-info/SOURCES.txt --- passlib-1.5.3/passlib.egg-info/SOURCES.txt 2011-10-08 04:58:49.000000000 +0000 +++ passlib-1.6.1/passlib.egg-info/SOURCES.txt 2012-08-02 19:28:07.000000000 +0000 @@ -4,19 +4,18 @@ README setup.cfg setup.py +tox.ini docs/conf.py -docs/conf.py.orig docs/contents.rst docs/copyright.rst docs/history.rst docs/index.rst docs/install.rst -docs/make.py docs/modular_crypt_format.rst docs/new_app_quickstart.rst -docs/notes.txt docs/overview.rst docs/password_hash_api.rst +docs/requirements.txt docs/_static/logo-128.png docs/_static/logo-64.png docs/_static/logo.ico @@ -26,16 +25,17 @@ docs/_static/masthead.svg docs/lib/passlib.apache.rst docs/lib/passlib.apps.rst -docs/lib/passlib.context-interface.rst -docs/lib/passlib.context-options.rst -docs/lib/passlib.context-usage.rst +docs/lib/passlib.context-tutorial.rst docs/lib/passlib.context.rst +docs/lib/passlib.exc.rst docs/lib/passlib.ext.django.rst docs/lib/passlib.hash.apr_md5_crypt.rst docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst docs/lib/passlib.hash.bcrypt.rst docs/lib/passlib.hash.bigcrypt.rst docs/lib/passlib.hash.bsdi_crypt.rst +docs/lib/passlib.hash.cisco_pix.rst +docs/lib/passlib.hash.cisco_type7.rst docs/lib/passlib.hash.crypt16.rst docs/lib/passlib.hash.cta_pbkdf2_sha1.rst docs/lib/passlib.hash.des_crypt.rst @@ -48,7 +48,12 @@ docs/lib/passlib.hash.ldap_other.rst docs/lib/passlib.hash.ldap_pbkdf2_digest.rst docs/lib/passlib.hash.ldap_std.rst +docs/lib/passlib.hash.lmhash.rst docs/lib/passlib.hash.md5_crypt.rst +docs/lib/passlib.hash.msdcc.rst +docs/lib/passlib.hash.msdcc2.rst +docs/lib/passlib.hash.mssql2000.rst +docs/lib/passlib.hash.mssql2005.rst docs/lib/passlib.hash.mysql323.rst docs/lib/passlib.hash.mysql41.rst docs/lib/passlib.hash.nthash.rst @@ -59,15 +64,16 @@ docs/lib/passlib.hash.plaintext.rst docs/lib/passlib.hash.postgres_md5.rst docs/lib/passlib.hash.rst +docs/lib/passlib.hash.scram.rst docs/lib/passlib.hash.sha1_crypt.rst docs/lib/passlib.hash.sha256_crypt.rst docs/lib/passlib.hash.sha512_crypt.rst docs/lib/passlib.hash.sun_md5_crypt.rst -docs/lib/passlib.hash.unix_fallback.rst +docs/lib/passlib.hash.unix_disabled.rst docs/lib/passlib.hosts.rst docs/lib/passlib.registry.rst +docs/lib/passlib.utils.compat.rst docs/lib/passlib.utils.des.rst -docs/lib/passlib.utils.h64.rst docs/lib/passlib.utils.handlers.rst docs/lib/passlib.utils.md4.rst docs/lib/passlib.utils.pbkdf2.rst @@ -76,9 +82,10 @@ passlib/apache.py passlib/apps.py passlib/context.py -passlib/default.cfg +passlib/exc.py passlib/hash.py passlib/hosts.py +passlib/ifc.py passlib/registry.py passlib/win32.py passlib.egg-info/PKG-INFO @@ -87,7 +94,6 @@ passlib.egg-info/top_level.txt passlib.egg-info/zip-safe passlib/_setup/__init__.py -passlib/_setup/cond2to3.py passlib/_setup/docdist.py passlib/_setup/stamp.py passlib/ext/__init__.py @@ -96,6 +102,7 @@ passlib/ext/django/utils.py passlib/handlers/__init__.py passlib/handlers/bcrypt.py +passlib/handlers/cisco.py passlib/handlers/des_crypt.py passlib/handlers/digests.py passlib/handlers/django.py @@ -103,35 +110,47 @@ passlib/handlers/ldap_digests.py passlib/handlers/md5_crypt.py passlib/handlers/misc.py +passlib/handlers/mssql.py passlib/handlers/mysql.py -passlib/handlers/nthash.py passlib/handlers/oracle.py passlib/handlers/pbkdf2.py passlib/handlers/phpass.py passlib/handlers/postgres.py passlib/handlers/roundup.py +passlib/handlers/scram.py passlib/handlers/sha1_crypt.py passlib/handlers/sha2_crypt.py passlib/handlers/sun_md5_crypt.py +passlib/handlers/windows.py passlib/tests/__init__.py passlib/tests/__main__.py passlib/tests/_test_bad_register.py -passlib/tests/genconfig.py +passlib/tests/backports.py +passlib/tests/sample1.cfg +passlib/tests/sample1b.cfg +passlib/tests/sample1c.cfg passlib/tests/sample_config_1s.cfg passlib/tests/test_apache.py passlib/tests/test_apps.py passlib/tests/test_context.py -passlib/tests/test_drivers.py +passlib/tests/test_context_deprecated.py passlib/tests/test_ext_django.py +passlib/tests/test_handlers.py passlib/tests/test_hosts.py passlib/tests/test_registry.py passlib/tests/test_utils.py +passlib/tests/test_utils_crypto.py passlib/tests/test_utils_handlers.py passlib/tests/test_win32.py +passlib/tests/tox_support.py passlib/tests/utils.py passlib/utils/__init__.py +passlib/utils/compat.py passlib/utils/des.py -passlib/utils/h64.py passlib/utils/handlers.py passlib/utils/md4.py -passlib/utils/pbkdf2.py \ No newline at end of file +passlib/utils/pbkdf2.py +passlib/utils/_blowfish/__init__.py +passlib/utils/_blowfish/_gen_files.py +passlib/utils/_blowfish/base.py +passlib/utils/_blowfish/unrolled.py \ No newline at end of file diff -Nru passlib-1.5.3/setup.cfg passlib-1.6.1/setup.cfg --- passlib-1.5.3/setup.cfg 2011-10-08 04:58:50.000000000 +0000 +++ passlib-1.6.1/setup.cfg 2012-08-02 19:28:07.000000000 +0000 @@ -4,12 +4,6 @@ [upload_docs] upload_dir = build/sphinx/html -[test] -test_suite = nose.collector - -[nosetests] -tests = passlib/tests - [egg_info] tag_build = tag_date = 0 diff -Nru passlib-1.5.3/setup.py passlib-1.6.1/setup.py --- passlib-1.5.3/setup.py 2011-10-07 03:07:39.000000000 +0000 +++ passlib-1.6.1/setup.py 2012-08-01 17:25:20.000000000 +0000 @@ -1,14 +1,14 @@ """passlib setup script""" -#========================================================= +#============================================================================= # init script env -- ensure cwd = root of source dir -#========================================================= +#============================================================================= import os root_dir = os.path.abspath(os.path.join(__file__,"..")) os.chdir(root_dir) -#========================================================= +#============================================================================= # imports -#========================================================= +#============================================================================= import re import sys import time @@ -19,44 +19,27 @@ from setuptools import setup has_distribute = True except ImportError: - from distutils import setup + from distutils.core import setup has_distribute = False - -#========================================================= + +#============================================================================= # init setup options -#========================================================= +#============================================================================= opts = { "cmdclass": { } } args = sys.argv[1:] -#========================================================= -# 2to3 translation -#========================================================= -if py3k: - # monkeypatch preprocessor into lib2to3 - from passlib._setup.cond2to3 import patch2to3 - patch2to3() - - # enable 2to3 translation in build_py - if has_distribute: - opts['use_2to3'] = True - else: - # if we can't use distribute's "use_2to3" flag, - # have to override build_py command - from distutils.command.build_py import build_py_2to3 as build_py - opts['cmdclass']['build_py'] = build_py - -#========================================================= -#register docdist command (not required) -#========================================================= +#============================================================================= +# register docdist command (not required) +#============================================================================= try: from passlib._setup.docdist import docdist opts['cmdclass']['docdist'] = docdist except ImportError: pass -#========================================================= +#============================================================================= # version string / datestamps -#========================================================= +#============================================================================= from passlib import __version__ as VERSION # if this is an hg checkout of passlib, add datestamp to version string. @@ -75,7 +58,7 @@ elif not v.startswith("-"): break i += 1 - + if for_release: assert '.dev' not in VERSION and '.post' not in VERSION else: @@ -91,28 +74,27 @@ # to have the correct version string from passlib._setup.stamp import stamp_distutils_output stamp_distutils_output(opts, VERSION) - -#========================================================= -#static text -#========================================================= -SUMMARY = "comprehensive password hashing framework supporting over 20 schemes" + +#============================================================================= +# static text +#============================================================================= +SUMMARY = "comprehensive password hashing framework supporting over 30 schemes" DESCRIPTION = """\ -Passlib is a password hashing library for Python 2 & 3, -which provides cross-platform implementations of over 20 -password hashing algorithms, as well as a framework for -managing existing password hashes. It's designed to be useful -for a wide range of tasks, from verifying a hash found in /etc/shadow, -to providing full-strength password hashing for multi-user application. +Passlib is a password hashing library for Python 2 & 3, which provides +cross-platform implementations of over 30 password hashing algorithms, as well +as a framework for managing existing password hashes. It's designed to be useful +for a wide range of tasks, from verifying a hash found in /etc/shadow, to +providing full-strength password hashing for multi-user application. * See the `online documentation `_ for details, installation instructions, and examples. -* See the `passlib homepage `_ +* See the `Passlib homepage `_ for the latest news, more information, and additional downloads. * See the `changelog `_ - for description of what's new in Passlib. + for a description of what's new in Passlib. All releases are signed with the gpg key `4CE1ED31 `_. @@ -130,10 +112,15 @@ Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 +Programming Language :: Python :: Implementation :: CPython +Programming Language :: Python :: Implementation :: Jython +Programming Language :: Python :: Implementation :: PyPy Topic :: Security :: Cryptography Topic :: Software Development :: Libraries """.splitlines() +# TODO: "Programming Language :: Python :: Implementation :: IronPython" -- issue 34 + is_release = False if '.dev' in VERSION: CLASSIFIERS.append("Development Status :: 3 - Alpha") @@ -143,12 +130,12 @@ is_release = True CLASSIFIERS.append("Development Status :: 5 - Production/Stable") -#========================================================= -#run setup -#========================================================= -# XXX: could omit 'passlib.setup' from eggs, but not sdist +#============================================================================= +# run setup +#============================================================================= +# XXX: could omit 'passlib._setup' from eggs, but not sdist setup( - #package info + # package info packages = [ "passlib", "passlib.ext", @@ -156,12 +143,13 @@ "passlib.handlers", "passlib.tests", "passlib.utils", + "passlib.utils._blowfish", "passlib._setup", ], - package_data = { "passlib": ["*.cfg" ], "passlib.tests": ["*.cfg"] }, + package_data = { "passlib.tests": ["*.cfg"] }, zip_safe=True, - #metadata + # metadata name = "passlib", version = VERSION, author = "Eli Collins", @@ -178,13 +166,13 @@ keywords = KEYWORDS, classifiers = CLASSIFIERS, - tests_require = 'nose >= 1.0', + tests_require = 'nose >= 1.1', test_suite = 'nose.collector', - #extra opts + # extra opts script_args=args, **opts ) -#========================================================= -#EOF -#========================================================= +#============================================================================= +# eof +#============================================================================= diff -Nru passlib-1.5.3/tox.ini passlib-1.6.1/tox.ini --- passlib-1.5.3/tox.ini 1970-01-01 00:00:00.000000000 +0000 +++ passlib-1.6.1/tox.ini 2012-08-02 17:41:11.000000000 +0000 @@ -0,0 +1,151 @@ +#=========================================================================== +# Passlib configuration for TOX +# ============================= +# +# PASSLIB_TEST_MODE: +# +# "quick" +# run the bare minimum tests to ensure functionality. +# variable-cost hashes are tested at their lowest setting. +# hash algorithms are only tested against the backend that will +# be used on the current host. no fuzz testing is done. +# +# "default" +# same as ``"quick"``, except: hash algorithms are tested +# at default levels, and a brief round of fuzz testing is done +# for each hash. +# +# "full" +# extra regression and internal tests are enabled, hash algorithms are tested +# against all available backends, unavailable ones are mocked whre possible, +# additional time is devoted to fuzz testing. +# +# testing of m2crypto integration - done in py27 test +# +# testing of django integration - split across various cpython tests: +# py25 - tests django 1.3 +# py26 - tests no django +# py27 - tests latest django +# +# testing of bcrypt backends - split across various cpython tests: +# py25 - tests builtin bcrypt +# py27 - tests py-bcrypt, bcryptor +#=========================================================================== + +#=========================================================================== +# global config +#=========================================================================== +[tox] +minversion=1.3 +envlist = py27,py32,py25,py26,py31,py33,pypy1.5,pypy16,pypy17,pypy18,pypy19,jython,gae25,gae27 + +#=========================================================================== +# stock CPython VMs +#=========================================================================== +[testenv] +setenv = + PASSLIB_TEST_MODE = full +changedir = {envdir} +commands = + nosetests {posargs:passlib.tests} +deps = + nose + coverage + unittest2 + +[testenv:py25] +deps = + nose + coverage + # unittest2 omitted, to test backport code + django<1.4 + +[testenv:py27] +deps = + nose + coverage + unittest2 + py-bcrypt + bcryptor + django + M2Crypto + +[testenv:py31] +deps = + nose + unittest2py3k + +[testenv:py32] +deps = + nose + coverage + unittest2py3k + +[testenv:py33] +deps = + nose + coverage + unittest2py3k + +#=========================================================================== +# PyPy VM - all releases currently target Python 2.7 +#=========================================================================== +[testenv:pypy15] +basepython = pypy1.5 + +[testenv:pypy16] +basepython = pypy1.6 + +[testenv:pypy17] +basepython = pypy1.7 + +[testenv:pypy18] +basepython = pypy1.8 + +[testenv:pypy19] +basepython = pypy1.9 + +#=========================================================================== +# Jython - no special directives, currently same as py25 +#=========================================================================== + +#=========================================================================== +# Google App Engine integration +#=========================================================================== +[testenv:gae25] +basepython = python2.5 +deps = + # FIXME: getting all kinds of errors when using nosegae 0.2.0 :( + nose + nosegae==0.1.9 + unittest2 +changedir = {envdir}/lib/python2.5/site-packages +commands = + # setup custom app.yaml so GAE can run + python -m passlib.tests.tox_support setup_gae . python + + # FIXME: have to run using --without-sandbox for now, + # something in nose+GAE+virtualenv won't play nice with eachother. + nosetests --with-gae --without-sandbox {posargs:passlib/tests} + +[testenv:gae27] +basepython = python2.7 +deps = + # FIXME: getting all kinds of errors when using nosegae 0.2.0 :( + nose + nosegae==0.1.9 + unittest2 +changedir = {envdir}/lib/python2.7/site-packages +commands = + # setup custom app.yaml so GAE can run + python -m passlib.tests.tox_support setup_gae . python27 + + # TODO: figure out how to select django 1.2 so the extension under gae. + + # FIXME: have to run using --without-sandbox for now, + # something in nose+GAE+virtualenv won't play nice with eachother. + nosetests --with-gae --without-sandbox {posargs:passlib/tests} + +#=========================================================================== +# eof +#===========================================================================