passlib-1.6.5/0000755000175000017500000000000012560246762014320 5ustar biscuitbiscuit00000000000000passlib-1.6.5/docs/0000755000175000017500000000000012560246762015250 5ustar biscuitbiscuit00000000000000passlib-1.6.5/docs/index.rst0000644000175000017500000000766012560150775017120 0ustar biscuitbiscuit00000000000000.. image:: _static/masthead.png :align: center ========================================== Passlib |release| documentation ========================================== Welcome ======= 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.3 ` 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 ================ .. table:: :class: fullwidth :column-alignment: lr ================ =================================================== Homepage: ``_ Online Docs: ``_ Discussion: ``_ ---------------- --------------------------------------------------- ---------------- --------------------------------------------------- Downloads: ``_ Source: ``_ ================ =================================================== passlib-1.6.5/docs/history.rst0000644000175000017500000000003012214647122017463 0ustar biscuitbiscuit00000000000000.. include:: ../CHANGES passlib-1.6.5/docs/copyright.rst0000644000175000017500000000003012214647122017772 0ustar biscuitbiscuit00000000000000.. include:: ../LICENSE passlib-1.6.5/docs/conf.py0000644000175000017500000002523612560246447016557 0ustar biscuitbiscuit00000000000000# -*- coding: utf-8 -*- """ Sphinx configuration file for the 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 # 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('.')) # make sure root of source dir in sys.path sys.path.insert(0, os.path.abspath(os.pardir)) #============================================================================= # imports #============================================================================= # build option flags: # "for-pypi" -- enable analytics tracker for pypi documentation 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.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', # add autodoc 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', # allow table column alignment styling 'cloud_sptheme.ext.table_styling', # modify logo per page 'cloud_sptheme.ext.perpage', ] # 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' # The frontpage document. index_doc = 'index' # General information about the project. project = 'Passlib' author = "Assurance Technologies, LLC" copyright = "2008-2015, " + author # 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. # release: The full version, including alpha/beta/rc tags. # version: The short X.Y version. from passlib import __version__ as release version = csp.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.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 # 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 = True keep_warnings = True issue_tracker_url = "bb:ecollins/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 = os.environ.get("SPHINX_THEME") or '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. html_theme_options = {} if csp.is_cloud_theme(html_theme): html_theme_options.update(roottarget=index_doc, issueicon=None, # lighter_decor=True, borderless_decor=True, sidebar_localtoc_title="Page contents", max_width="12in", hyphenation_language="en", ) 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 = [csp.get_theme_dir()] # The name for this set of Sphinx documents. If None, it defaults to # " v 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 = "%s %s Documentation" % (project, version) # 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") perpage_html_logo = { 'index': None, } # 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. common_sidebars = ['quicklinks.html', 'searchbox.html'] html_sidebars = { '**': ['localtoc.html', 'relations.html'] + common_sidebars, 'py-modindex': common_sidebars, 'genindex': common_sidebars, 'search': common_sidebars, } #html_sidebars = {'**': ['globaltoc.html', 'searchbox.html']} # 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 = [ (master_doc, project + '.tex', project + ' Documentation', author, '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 = [ (master_doc, project, project + ' Documentation', [author], 1) ] #============================================================================= # EOF #============================================================================= passlib-1.6.5/docs/lib/0000755000175000017500000000000012560246762016016 5ustar biscuitbiscuit00000000000000passlib-1.6.5/docs/lib/passlib.hash.cta_pbkdf2_sha1.rst0000644000175000017500000000442712257351267024050 0ustar biscuitbiscuit00000000000000================================================================= :class:`passlib.hash.cta_pbkdf2_sha1` - Cryptacular's PBKDF2 hash ================================================================= .. index:: pbkdf2 hash; Cryptacular .. currentmodule:: passlib.hash This class provides an implementation of Cryptacular's PBKDF2-HMAC-SHA1 hash format [#cta]_. PBKDF2 is a key derivation function [#pbkdf2]_ that is ideally suited as the basis for a password hash, as it provides variable length salts, variable number of rounds. .. seealso:: * :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. * :doc:`dlitz_pbkdf2_sha1 ` for another hash which looks almost exactly like this one. Interface ========= .. autoclass:: cta_pbkdf2_sha1() Format & Algorithm ================== A example hash (of ``password``) is: ``$p5k2$2710$oX9ZZOcNgYoAsYL-8bqxKg==$AU2JLf2rNxWoZxWxRCluY0u6h6c=`` All of this scheme's hashes have the format :samp:`$p5k2${rounds}${salt}${checksum}`, where: * ``$p5k2$`` is used as the :ref:`modular-crypt-format` identifier. * :samp:`{rounds}` is the number of PBKDF2 iterations to perform, stored as lowercase hexadecimal number with no zero-padding (in the example: ``2710`` or 10000 iterations). * :samp:`{salt}` is the salt string encoding using base64 (with ``-_`` as the high values). ``oX9ZZOcNgYoAsYL-8bqxKg==`` in the example. * :samp:`{checksum}` is 28 characters encoding the resulting 20-byte PBKDF2 derived key using base64 (with ``-_`` as the high values). ``AU2JLf2rNxWoZxWxRCluY0u6h6c=`` in the example. In order to generate the checksum, the password is first encoded into UTF-8 if it's unicode. The salt is decoded from its base64 representation. PBKDF2 is called using the encoded password, the full salt, the specified number of rounds, and using HMAC-SHA1 as its pseudorandom function. 20 bytes of derived key are requested, and the resulting key is encoded and used as the checksum portion of the hash. .. rubric:: Footnotes .. [#cta] The reference for this hash format - ``_. .. [#pbkdf2] The specification for the PBKDF2 algorithm - ``_. passlib-1.6.5/docs/lib/passlib.hash.mysql323.rst0000644000175000017500000000451712257351267022532 0ustar biscuitbiscuit00000000000000.. index:: MySQL; OLD_PASSWORD() ======================================================================== :class:`passlib.hash.mysql323` - MySQL 3.2.3 password hash ======================================================================== .. currentmodule:: passlib.hash .. 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. This class implements the first of MySQL's password hash functions, used to store its 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 >>> # encrypt password >>> mysql323.encrypt("password") '5d2e19393cc5ef67' >>> # verify correct password >>> mysql323.verify("password", '5d2e19393cc5ef67') True >>> mysql323.verify("secret", '5d2e19393cc5ef67') False .. seealso:: * :ref:`password hash usage ` -- for more usage examples * :mod:`passlib.apps` -- for a list of predefined :ref:`mysql contexts `. Interface ========= .. autoclass:: mysql323() Format & Algorithm ================== A mysql-323 password hash consists of 16 hexadecimal 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). The algorithm used is extremely simplistic, for details, see the source implementation in the footnotes [#f1]_. Security Issues =============== Lacking any sort of salt, ignoring all whitespace, and having a simplistic algorithm that amounts to little more than a checksum, this is not secure, and should not be used for *any* purpose but verifying existing MySQL 3.2.3 - 4.0 password hashes. .. rubric:: Footnotes .. [#f1] Source of implementation used by Passlib - ``_ .. [#f2] Mysql document describing transition - ``_ passlib-1.6.5/docs/lib/passlib.hash.phpass.rst0000644000175000017500000000553712257351267022436 0ustar biscuitbiscuit00000000000000.. index:: PHPass; portable hash, phpBB3; PHPass hash ================================================================== :class:`passlib.hash.phpass` - PHPass' Portable Hash ================================================================== .. currentmodule:: passlib.hash This algorithm is used primarily by PHP software 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 its other hashes are available. Due to its reliance on MD5, and the simplistic implementation, other hash algorithms should be used if possible. .. seealso:: :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. Interface ========= .. autoclass:: phpass() Format ================== An example hash (of ``password``) is ``$P$8ohUJ.1sdFw09/bMaAQPTGDNi2BIUt1``. A phpass portable hash string has the format :samp:`$P${rounds}{salt}{checksum}`, where: * ``$P$`` is the prefix used to identify phpass hashes, following the :ref:`modular-crypt-format`. * :samp:`{rounds}` is a single character encoding a 6-bit integer representing the number of rounds used. This is logarithmic, the real number of rounds is ``2**rounds``. (in the example, rounds is encoded as ``8``, or 2**13 iterations). * :samp:`{salt}` is eight characters drawn from ``[./0-9A-Za-z]``, providing a 48-bit salt (``ohUJ.1sd`` in the example). * :samp:`{checksum}` is 22 characters drawn from the same set, encoding the 128-bit checksum (``Fw09/bMaAQPTGDNi2BIUt1`` in the example). .. note:: Note that phpBB3 databases uses the alternate prefix ``$H$``, both prefixes are recognized by this implementation, and the checksums are the same. Algorithm ========= PHPass uses a straightforward algorithm to calculate the checksum: 1. an initial result is generated from the MD5 digest of the salt string + the secret. 2. for :samp:`2**{rounds}` iterations, a new result is created from the MD5 digest of the last result + the password. 3. the last result is then encoded according to the format described above. Deviations ========== This implementation of phpass differs from the specification in one way: * 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 known reference hashes. In order to provide support for unicode strings, 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. .. rubric:: Footnotes .. [#pp] PHPass homepage, which describes the Portable Hash algorithm - ``_ passlib-1.6.5/docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst0000644000175000017500000000370212257351267025253 0ustar biscuitbiscuit00000000000000=========================================================================== :class:`passlib.hash.atlassian_pbkdf2_sha1` - Atlassian's PBKDF2-based Hash =========================================================================== .. index:: pair: Atlassian; pbkdf2 hash .. currentmodule:: passlib.hash This class provides an implementation of the PBKDF2 based hash used by Atlassian in Jira and other products. Note that unlike the most PBKDF2 hashes supported by Passlib, this one uses a fixed number of rounds (10000). That is currently a sufficient amount, but it cannot be altered; so this scheme should only be used to read existing hashes, and not used in new applications. .. seealso:: * :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. * :doc:`passlib.hash.pbkdf2_{digest} ` -- for some other PBKDF2-based hashes. Interface ========= .. autoclass:: atlassian_pbkdf2_sha1() Format & Algorithm ================== 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. A example hash (of ``password``) is: ``{PKCS5S2}DQIXJU038u4P7FdsuFTY/+35bm41kfjZa57UrdxHp2Mu3qF2uy+ooD+jF5t1tb8J`` Once decoded, the salt value (in hexadecimal octets) is: ``0d0217254d37f2ee0fec576cb854d8ff`` and the checksum value (in hexadecimal octets) is: ``edf96e6e3591f8d96b9ed4addc47a7632edea176bb2fa8a03fa3179b75b5bf09`` When calculating the checksum: the password is encoded into UTF-8 if not already encoded. Using the specified salt, and a fixed 10000 rounds, PBKDF2-HMAC-SHA1 is used to generate a 32 byte key, which appended to the salt and encoded in base64. .. rubric:: Footnotes .. [#pbkdf2] The specification for the PBKDF2 algorithm - ``_. passlib-1.6.5/docs/lib/passlib.hash.oracle10.rst0000644000175000017500000001140412257351267022534 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.oracle10` - Oracle 10g password hash ================================================================== .. currentmodule:: passlib.hash .. warning:: This hash is not secure, and should not be used for any purposes besides manipulating existing Oracle 10 password hashes. This class implements the hash algorithm used by the Oracle Database up to version 10g Rel.2. It was superseded 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 oracle10 >>> # encrypt password using specified username >>> hash = oracle10.encrypt("password", user="username") >>> hash '872805F3F4C83365' >>> # verify correct password >>> oracle10.verify("password", hash, user="username") True >>> # verify correct password w/ wrong username >>> oracle10.verify("password", hash, user="somebody") False >>> # 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 its behavior may not match under various border cases. *caveat emptor*. Interface ========= .. autoclass:: oracle10() .. rst-class:: html-toggle Format & Algorithm ================== Oracle10 hashes all consist of a series of 16 hexadecimal digits, representing the resulting checksum. Oracle10 hashes can be formed by the following procedure: 1. Concatenate the username and password together. 2. Convert the result to upper case 3. Encoding the result in a multi-byte format [#enc]_ such that ascii characters (eg: ``USER``) are represented with additional null bytes inserted (eg: ``\x00U\x00S\x00E\x00R``). 4. Right-pad the result with null bytes, to bring the total size to an integer multiple of 8. this is the final input string. 5. The input string is then encoded using DES in CBC mode. The string ``\x01\x23\x45\x67\x89\xAB\xCD\xEF`` is used as the DES key, and a block of null bytes is used as the CBC initialization vector. All but the last block of ciphertext is discarded. 6. The input string is then run through DES-CBC a second time; this time the last block of ciphertext from step 5 is used as the DES key, a block of null bytes is still used as the CBC initialization vector. All but the last block of ciphertext is discarded. 7. The last block of ciphertext of step 6 is converted to a hexadecimal string, and returned as the checksum. Security Issues =============== This algorithm it not suitable for *any* use besides manipulating existing Oracle10 account passwords, due to the following flaws [#flaws]_: * Its use of the username as a salt value means that common usernames (e.g. ``system``) will occur more frequently as salts, weakening the effectiveness of the salt in foiling pre-computed tables. * 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. * Its 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 implementation in unknown ways, as there is no official documentation. There is only one known issue: * Unicode Policy 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]_ 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. This note applies as well to any provided username, as they are run through the same policy. .. 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, and supports unicode input. .. [#flaws] Whitepaper analyzing flaws in this algorithm - ``_. .. [#] Description of Oracle10g and Oracle11g algorithms - ``_. passlib-1.6.5/docs/lib/passlib.hash.postgres_md5.rst0000644000175000017500000000542212257351267023544 0ustar biscuitbiscuit00000000000000.. index:: Postgres; md5 hash ================================================================== :class:`passlib.hash.postgres_md5` - PostgreSQL MD5 password hash ================================================================== .. warning:: This hash is not secure, and should not be used for any purposes besides manipulating existing PostgreSQL password hashes. .. currentmodule:: passlib.hash This class implements the md5-based hash algorithm used by PostgreSQL to store its user account passwords. This scheme was introduced in PostgreSQL 7.2; prior to this PostgreSQL stored its 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 >>> # encrypt password using specified username >>> hash = postgres_md5.encrypt("password", user="username") >>> hash 'md55a231fcdb710d73268c4f44283487ba2' >>> # verify correct password >>> postgres_md5.verify("password", hash, user="username") True >>> # verify correct password w/ wrong username >>> postgres_md5.verify("password", hash, user="somebody") False >>> # verify incorrect password >>> postgres_md5.verify("password", hash, user="username") False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: postgres_md5() Format & Algorithm ================== Postgres-MD5 hashes all have the format :samp:`md5{checksum}`, where :samp:`{checksum}` is 32 hexadecimal digits, encoding a 128-bit checksum. This checksum is the MD5 message digest of the password concatenated with the username. Security Issues =============== This algorithm it not suitable for *any* use besides manipulating existing PostgreSQL account passwords, due to the following flaws: * Its use of the username as a salt value means that common usernames (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, existing MD5 lookup tables have an increased chance of being able to reverse common hashes. * Its simplicity makes high-speed brute force attacks much more feasible [#brute]_ . .. rubric:: Footnotes .. [#] Discussion leading up to design of algorithm - ``_ .. [#] Message explaining postgres md5 hash algorithm - ``_ .. [#brute] Blog post demonstrating brute-force attack ``_. passlib-1.6.5/docs/lib/passlib.hash.mssql2000.rst0000644000175000017500000000656412257351267022602 0ustar biscuitbiscuit00000000000000================================================================== :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 its 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 hexadecimal 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 hexadecimal, 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. * Its 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 - ``_. passlib-1.6.5/docs/lib/passlib.apps.rst0000644000175000017500000002202512555044153021142 0ustar biscuitbiscuit00000000000000================================================================== :mod:`passlib.apps` - Helpers for various applications ================================================================== .. module:: passlib.apps :synopsis: encrypting & verifying passwords used in sql servers and other applications .. _predefined-context-example: 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. .. 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 custom_app_context object, >>> # a helper to let new applications *quickly* add password hashing. >>> from passlib.apps import custom_app_context Encrypting a password is simple (and salt generation is handled automatically):: >>> hash = custom_app_context.encrypt("toomanysecrets") >>> hash '$5$rounds=84740$fYChCy.52EzebF51$9bnJrmTf2FESI93hgIBFF4qAfysQcKoB0veiI0ZeYU4' Verifying a password against an existing hash is just as quick:: >>> custom_app_context.verify("toomanysocks", hash) False >>> custom_app_context.verify("toomanysecrets", hash) True .. seealso:: the :ref:`CryptContext Tutorial ` and :ref:`CryptContext Reference ` for more information about the CryptContext class. .. 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:: django16_context The object replicates the stock password hashing policy for Django 1.6. It supports all the Django 1.0-1.6 hashes, and defaults to :class:`~passlib.hash.django_pbkdf2_sha256`. It treats all Django 1.0 hashes as deprecated. .. versionadded:: 1.6.2 .. data:: django_context 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.2 This now points to :data:`django16_context`. .. _ldap-contexts: LDAP ==== Passlib provides two contexts related to ldap hashes: .. data:: ldap_context This object provides a pre-configured :class:`!CryptContext` instance for handling LDAPv2 password hashes. It recognizes all the :ref:`standard ldap hashes `. It defaults to using the ``{SSHA}`` password hash. For times when there should be another default, using code such as the following:: >>> from passlib.apps import ldap_context >>> ldap_context = ldap_context.replace(default="ldap_salted_md5") >>> # the new context object will now default to {SMD5}: >>> ldap_context.encrypt("password") '{SMD5}T9f89F591P3fFh1jz/YtW4aWD5s=' .. data:: ldap_nocrypt_context This object recognizes all the standard ldap schemes that :data:`!ldap_context` does, *except* for the ``{CRYPT}``-based schemes. .. index:: MySQL; crypt context .. _mysql-contexts: MySQL ===== This module provides two pre-configured :class:`!CryptContext` instances for handling MySQL user passwords: .. data:: mysql_context This object should recognize the new :class:`~passlib.hash.mysql41` hashes, as well as any legacy :class:`~passlib.hash.mysql323` hashes. It defaults to mysql41 when generating new hashes. This should be used with MySQL version 4.1 and newer. .. data:: mysql3_context This object is for use with older MySQL deploys which only recognize the :class:`~passlib.hash.mysql323` hash. 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 PHPass ====== `PHPass `_ is a PHP password hashing library, and hashes derived from it are found in a number of PHP applications. It is found in a wide range of PHP applications, including Drupal and Wordpress. .. data:: phpass_context This object following the standard PHPass logic: it supports :class:`~passlib.hash.bcrypt`, :class:`~passlib.hash.bsdi_crypt`, and implements an custom scheme called the "phpass portable hash" :class:`~passlib.hash.phpass` as a fallback. BCrypt is used as the default if support is available, otherwise the Portable Hash will be used as the default. .. versionchanged:: 1.5 Now uses Portable Hash as fallback if BCrypt isn't available. Previously used BSDI-Crypt as fallback (per original PHPass implementation), but it was decided PHPass is in fact more secure. .. data:: phpbb3_context This object supports phpbb3 password hashes, which use a variant of :class:`~passlib.hash.phpass`. .. index:: Postgres; crypt context PostgreSQL ========== .. data:: postgres_context This object should recognize password hashes stores in PostgreSQL's ``pg_shadow`` table; which are all assumed to follow the :class:`~passlib.hash.postgres_md5` format. Note that the username must be provided whenever encrypting or verifying a postgres hash:: >>> from passlib.apps import postgres_context >>> # encrypting a password... >>> postgres_context.encrypt("somepass", user="dbadmin") 'md578ed0f0ab2be0386645c1b74282917e7' >>> # verifying a password... >>> postgres_context.verify("somepass", 'md578ed0f0ab2be0386645c1b74282917e7', user="dbadmin") True >>> postgres_context.verify("wrongpass", 'md578ed0f0ab2be0386645c1b74282917e7', user="dbadmin") False >>> # 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 ======= The `Roundup Issue Tracker `_ has long supported a series of different methods for encoding passwords. The following contexts are available for reading Roundup password hash fields: .. data:: roundup10_context This object should recognize all password hashes used by Roundup 1.4.16 and earlier: :class:`~passlib.hash.ldap_hex_sha1` (the default), :class:`~passlib.hash.ldap_hex_md5`, :class:`~passlib.hash.ldap_des_crypt`, and :class:`~passlib.hash.roundup_plaintext`. .. data:: roundup15_context Roundup 1.4.17 adds support for :class:`~passlib.hash.ldap_pbkdf2_sha1` as its preferred hash format. This context supports all the :data:`roundup10_context` hashes, but adds that hash as well (and uses it as the default). .. data:: roundup_context 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. passlib-1.6.5/docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst0000644000175000017500000000620212555044153024412 0ustar biscuitbiscuit00000000000000.. index:: pbkdf2 hash; dlitz =========================================================================== :class:`passlib.hash.dlitz_pbkdf2_sha1` - Dwayne Litzenberger's PBKDF2 hash =========================================================================== .. warning:: Due to a small flaw, this hash is not as strong as other PBKDF1-HMAC-SHA1 based hashes. It should probably not be used for new applications. .. currentmodule:: passlib.hash This class provides an implementation of Dwayne Litzenberger's PBKDF2-HMAC-SHA1 hash format [#dlitz]_. PBKDF2 is a key derivation function [#pbkdf2]_ that is ideally suited as the basis for a password hash, as it provides variable length salts, variable number of rounds. .. seealso:: * :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. * :doc:`cta_pbkdf2_sha1 ` for another hash which looks almost exactly like this one. Interface ========= .. autoclass:: dlitz_pbkdf2_sha1() Format & Algorithm ================== A example hash (of ``password``) is: ``$p5k2$2710$.pPqsEwHD7MiECU0$b8TQ5AMQemtlaSgegw5Je.JBE3QQhLbO``. All of this scheme's hashes have the format :samp:`$p5k2${rounds}${salt}${checksum}`, where: * ``$p5k2$`` is used as the :ref:`modular-crypt-format` identifier. * :samp:`{rounds}` is the number of PBKDF2 iterations to perform, stored as lowercase hexadecimal 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 :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.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. Then, the entire configuration string (all of the hash except the checksum, ie :samp:`$p5k2${rounds}${salt}`) is used as the PBKDF2 salt. PBKDF2 is called using the encoded password, the full salt, the specified number of rounds, and using HMAC-SHA1 as its pseudorandom function. 24 bytes of derived key are requested, and the resulting key is encoded and used as the checksum portion of the hash. Security Issues =============== * *Extra Block:* This hash generates 24 bytes using PBKDF2-HMAC-SHA1. Since SHA1 has a digest size of only 20 bytes, this means an second PBKDF2 block must be generated for each :class:`dlitz_pbkdf2_sha1` hash. While a normal user has to calculate both blocks, a dedicated attacker would only have to calculate the first block when brute-forcing, taking half the time. That means this hash is half as strong as other PBKDF2-HMAC-SHA1 based hashes (given a fixed amount of time spent by the user). .. rubric:: Footnotes .. [#dlitz] The reference for this hash format - ``_. .. [#pbkdf2] The specification for the PBKDF2 algorithm - ``_. passlib-1.6.5/docs/lib/passlib.utils.compat.rst0000644000175000017500000000337612214647123022627 0ustar biscuitbiscuit00000000000000====================================================== :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). passlib-1.6.5/docs/lib/passlib.hash.ldap_other.rst0000644000175000017500000000332612257351267023253 0ustar biscuitbiscuit00000000000000=============================================================== :samp:`passlib.hash.ldap_{other}` - Non-Standard RFC2307 Hashes =============================================================== .. currentmodule:: passlib.hash This section as a catch-all for a number of password hash formats supported by Passlib which use :rfc:`2307` style encoding, but are not part of any standard. .. seealso:: * :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. Hexadecimal Digests =================== All of the digests specified in RFC 2307 use base64 encoding. The following are non-standard versions which use hexadecimal encoding, as is found in some applications. .. class:: ldap_hex_md5 hexadecimal version of :class:`ldap_md5`, this is just the md5 digest of the password. an example hash (of ``password``) is ``{MD5}5f4dcc3b5aa765d61d8327deb882cf99``. .. class:: ldap_hex_sha1 hexadecimal version of :class:`ldap_sha1`, this is just the sha1 digest of the password. an example hash (of ``password``) is ``{SHA}5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8``. Other Hashes ============ .. class:: roundup_plaintext RFC 2307 specifies plaintext passwords should be stored without any identifying prefix. This class implements an alternate method used by the Roundup Issue Tracker [#roundup]_, which (when storing plaintext passwords) uses the identifying prefix ``{plaintext}``. an example hash (of ``password``) is ``{plaintext}password``. .. rubric:: Footnotes .. [#roundup] Roundup Issue Tracker homepage - ``_. passlib-1.6.5/docs/lib/passlib.exc.rst0000644000175000017500000000153012555044153020754 0ustar biscuitbiscuit00000000000000=============================================================== :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 .. autoexception:: PasslibSecurityError Warnings ======== .. autoexception:: PasslibWarning Minor Warnings -------------- .. autoexception:: PasslibConfigWarning .. autoexception:: PasslibHashWarning Critical Warnings ----------------- .. autoexception:: PasslibRuntimeWarning .. autoexception:: PasslibSecurityWarning passlib-1.6.5/docs/lib/passlib.hash.sha256_crypt.rst0000644000175000017500000001135112257351267023360 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.sha256_crypt` - SHA-256 Crypt ================================================================== .. currentmodule:: passlib.hash 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() .. 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 ================== 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 unambiguously 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 .. [#f1] Ulrich Drepper's SHA-256/512-Crypt specification, reference implementation, and test vectors - `sha-crypt specification `_ passlib-1.6.5/docs/lib/passlib.apache.rst0000644000175000017500000000552212553722451021425 0ustar biscuitbiscuit00000000000000============================================= :mod:`passlib.apache` - Apache Password Files ============================================= .. module:: passlib.apache :synopsis: reading/writing htpasswd & htdigest files This module provides utilities for reading and writing Apache's htpasswd and htdigest files; though the use of two helper classes. .. 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 ============== The :class:`!HTpasswdFile` class allows managing of htpasswd files. A quick summary of its usage:: >>> from passlib.apache import HtpasswdFile >>> # 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 >>> ht = HtpasswdFile("test.htpasswd") >>> ht.set_password("someuser", "new secret password") >>> ht.save() >>> # examining file, verifying user's password >>> ht = HtpasswdFile("test.htpasswd") >>> ht.users() [ "someuser" ] >>> ht.check_password("someuser", "wrong password") False >>> ht.check_password("someuser", "new secret password") True >>> # making in-memory changes and exporting to string >>> ht = HtpasswdFile() >>> ht.set_password("someuser", "mypass") >>> ht.set_password("someuser", "anotherpass") >>> print ht.to_string() someuser:$apr1$T4f7D9ly$EobZDROnHblCNPCtrgh5i/ anotheruser:$apr1$vBdPWvh1$GrhfbyGvN/7HalW5cS9XB1 .. warning:: :class:`!HtpasswdFile` currently defaults to using :class:`!apr_md5_crypt`, as this is the only htpasswd hash guaranteed to be portable across operating systems. However, for security reasons Passlib 1.7 will default to using the strongest algorithm available on the host platform (e.g. :class:`!bcrypt` or :class:`!sha256_crypt`). Applications that are relying on the old behavior should specify ``HtpasswdFile(default_scheme="portable")`` (new in Passlib 1.6.3). .. autoclass:: HtpasswdFile(path=None, new=False, autosave=False, ...) .. index:: Apache; htdigest Htdigest Files ============== The :class:`!HtdigestFile` class allows management of htdigest files in a similar fashion to :class:`HtpasswdFile`. .. autoclass:: HtdigestFile(path, default_realm=None, new=False, autosave=False, ...) .. rubric:: Footnotes .. [#] Htpasswd Manual - ``_ .. [#] Apache Auth Configuration - ``_ passlib-1.6.5/docs/lib/passlib.hash.bigcrypt.rst0000644000175000017500000001335012257351267022753 0ustar biscuitbiscuit00000000000000======================================================================= :class:`passlib.hash.bigcrypt` - BigCrypt ======================================================================= .. warning:: This algorithm is dangerously weak, and should not be used if at all possible. .. 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() 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 :data:`hash64 `-encoded 12-bit integer (``S/`` in the example). * each :samp:`{checksum_i}` is a separate checksum, stored as an 11 character :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 ========= The bigcrypt algorithm is designed to re-use the original des-crypt algorithm: 1. Given a password string and a salt string. 2. The password is NULL padded at the end to the smallest non-zero multiple of 8 bytes. 3. 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 the most significant 7 bits, and the 8th character providing the least significant 7 bits. 4. The 2 character salt string is decoded to a 12-bit integer salt value; The salt string uses little-endian :data:`hash64 ` encoding. 5. 25 repeated rounds of modified DES encryption are performed; starting with a null input block, and using the 56-bit integer from step 3 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. 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 :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), an additional checksum is generated by repeating steps 3..7, with the following changes: a. Step 3 uses the specified 8 bytes of the password, instead of the first 8 bytes. b. Step 4 uses the first two characters from the previous checksum as the salt for the next checksum. 9. The final checksum string is the concatenation of the checksum segments generated from steps 7 and 8, in order. .. note:: Because of the chained structure, bigcrypt has the property that the first 13 characters of any bigcrypt hash form a valid :class:`~passlib.hash.des_crypt` hash of the same password; and bigcrypt hashes of any passwords less than 9 characters will be identical to des-crypt. Security Issues =============== BigCrypt is dangerously flawed: * It suffers from all the flaws of :class:`~passlib.hash.des_crypt`. * Since each checksum component in its hash is essentially a separate des-crypt checksum, they can be attacked in parallel. * It reveals information about the length of the encoded password (to within 8 characters), further reducing the keyspace that needs to be searched for each of the individual segments. * The last checksum typically contains only a few characters of the passphrase, and once cracked, can be used to narrow the overall keyspace. Deviations ========== This implementation of bigcrypt differs from others in two ways: * Maximum Password Size: This implementation currently accepts arbitrarily large passwords, producing arbitrarily large hashes. Other implementation have 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 to be truncated to their specific maximum length. * Unicode Policy: The original bigcrypt algorithm was designed for 7-bit ``us-ascii`` encoding only (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`` before running them through bigcrypt. If a different encoding is desired by an application, the password should be encoded before handing it to Passlib. .. rubric:: Footnotes .. [#] discussion of bigcrypt & crypt16 - ``_ passlib-1.6.5/docs/lib/passlib.hash.apr_md5_crypt.rst0000644000175000017500000000334312257351267023701 0ustar biscuitbiscuit00000000000000.. 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 its author, who urges migration to newer hash algorithms. .. currentmodule:: passlib.hash 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 the MD5-Crypt algorithm, and should be considered just as weak as MD5-Crypt itself. .. seealso:: * :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() Format & Algorithm ================== This format and algorithm of Apache's MD5-Crypt is identical to the original MD5-Crypt, except for two changes: 1. The encoded string uses ``$apr1$`` as its prefix, while md5-crypt uses ``$1$``. 2. The algorithm uses ``$apr1$`` as a constant in the step where md5-crypt uses ``$1$`` in its calculation of digest B (see the :ref:`md5-crypt algorithm `). Because of this change, even raw checksums generated by apr-md5-crypt and md5-crypt are not compatible with each other. See :doc:`md5_crypt ` for the format & algorithm descriptions, as well as security notes. .. rubric:: Footnotes .. [#] Apache's description of Apr-MD5-Crypt - ``_ passlib-1.6.5/docs/lib/passlib.hash.msdcc2.rst0000644000175000017500000001004112257351267022275 0ustar biscuitbiscuit00000000000000.. 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 its 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 hexadecimal 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 identical 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 hexadecimal; 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 piece 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 - ``_ passlib-1.6.5/docs/lib/passlib.hash.sha1_crypt.rst0000644000175000017500000001054412257351267023207 0ustar biscuitbiscuit00000000000000=================================================================== :class:`passlib.hash.sha1_crypt` - SHA-1 Crypt =================================================================== .. currentmodule:: passlib.hash SHA1-Crypt is a hash algorithm introduced by NetBSD in 2004. It's based on a variation of the PBKDF1 algorithm, and supports a large salt and variable number of rounds. .. seealso:: :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. Interface ========= .. 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 ====== An example hash (of ``password``) is ``$sha1$40000$jtNX3nZ2$hBNaIXkt4wBI2o5rsi8KejSjNqIq``. An sha1-crypt hash string has the format :samp:`$sha1${rounds}${salt}${checksum}`, where: * ``$sha1$`` is the prefix used to identify sha1-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-64 characters drawn from ``[./0-9A-Za-z]`` (``jtNX3nZ2`` in the example). * :samp:`{checksum}` is 28 characters drawn from the same set, encoding a 168-bit checksum. (``hBNaIXkt4wBI2o5rsi8KejSjNqIq/`` in the example). .. rst-class:: html-toggle Algorithm ========= The checksum is calculated using a modified version of PBKDF1 [#pbk]_, replacing its use of the SHA1 message digest with HMAC-SHA1, (which does not suffer from the current vulnerabilities that SHA1 itself does, as well as providing some of the advancements made in PBKDF2). * first, the HMAC-SHA1 digest of :samp:`{salt}$sha1${rounds}` is generated, using the password as the HMAC-SHA1 key. * then, for :samp:`{rounds}-1` iterations, the previous HMAC-SHA1 digest is fed back through HMAC-SHA1, again using the password as the HMAC-SHA1 key. * the checksum is then rendered into hash-64 format using an ordering that roughly corresponds to big-endian encoding of 24-bit chunks (see :data:`passlib.hash.sha1_crypt._chk_offsets` for exact byte order). Deviations ========== This implementation of sha1-crypt differs from the NetBSD implementation in a few ways: * Default Rounds: 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, via the :class:`CryptContext` class, and so it omitted from this implementation. * 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 in a hash have leading zeros. * Restricted salt string character set: The underlying algorithm can unambiguously 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 sha1-crypt generate and expect salts containing those characters. * 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 known reference hashes. In order to provide support for unicode strings, 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. .. rubric:: Footnotes .. [#desc] description of sha1-crypt algorithm - ``_ .. [#source] NetBSD implementation of SHA1-Crypt - ``_ .. [#pbk] rfc defining PBKDF1 & PBKDF2 - ``_ - passlib-1.6.5/docs/lib/passlib.utils.rst0000644000175000017500000001232412257351267021346 0ustar biscuitbiscuit00000000000000============================================= :mod:`passlib.utils` - Helper Functions ============================================= .. module:: passlib.utils :synopsis: internal helpers for implementing password hashes .. warning:: This module is primarily used as an internal support module. Its 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. This module primarily contains utility functions used internally by Passlib. However, end-user applications may find some of the functions useful, in particular: * :func:`consteq` * :func:`saslprep` * :func:`generate_password` Constants ========= .. .. data:: sys_bits 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 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 :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. .. seealso:: :ref:`mcf-identifiers` for a table of which OSes are known to support which hashes. .. PYPY JYTHON rounds_cost_values .. Decorators ========== .. autofunction:: classproperty Unicode Helpers =============== .. autofunction:: consteq .. autofunction:: saslprep 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 salt strings and other things which don't require a cryptographically strong source of randomness. If :func:`os.urandom` support is available, this will be an instance of :class:`!random.SystemRandom`, otherwise it will use the default python PRNG class, seeded from various sources at startup. .. autofunction:: getrandbytes .. autofunction:: getrandstr .. autofunction:: generate_password(size=10, charset=) Interface Tests =============== .. autofunction:: is_crypt_handler .. autofunction:: is_crypt_context .. autofunction:: has_rounds_info .. autofunction:: has_salt_info Submodules ========== There are also a few sub modules which provide additional utility functions: .. toctree:: :maxdepth: 1 passlib.utils.handlers passlib.utils.des passlib.utils.pbkdf2 .. passlib.utils.compat passlib-1.6.5/docs/lib/passlib.hash.nthash.rst0000644000175000017500000000541512257351267022420 0ustar biscuitbiscuit00000000000000.. index:: Windows; NT hash ================================================================== :class:`passlib.hash.nthash` - Windows' NT-HASH ================================================================== .. versionadded:: 1.6 .. warning:: 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. .. 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 >>> # encrypt password >>> h = nthash.encrypt("password") >>> h '8846f7eaee8fb117ad06bdd830b7586c' >>> # verify password >>> nthash.verify("password", h) True >>> nthash.verify("secret", h) False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: nthash() Format & Algorithm ================== A nthash consists of 32 hexadecimal digits, which encode the digest. An example hash (of ``password``) is ``8846f7eaee8fb117ad06bdd830b7586c``. The digest is calculated by encoding the secret using ``UTF-16-LE``, taking the :mod:`~passlib.utils.md4` digest, and then encoding that as hexadecimal. 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`. 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. .. versionchanged:: 1.6 This hash was named ``nthash`` under previous releases of Passlib. Security Issues =============== 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. passlib-1.6.5/docs/lib/passlib.hash.sun_md5_crypt.rst0000644000175000017500000002106312257351267023723 0ustar biscuitbiscuit00000000000000.. index:: Solaris; sun_md5_crypt ================================================================= :class:`passlib.hash.sun_md5_crypt` - Sun MD5 Crypt ================================================================= .. currentmodule:: passlib.hash This algorithm was developed by Alec Muffett [#mct]_ for Solaris, as a replacement for the aging :class:`~passlib.hash.des_crypt`. It was introduced in Solaris 9u2. While based on the MD5 message digest, it has very little at all in common with the :class:`~passlib.hash.md5_crypt` algorithm. It supports 32 bit variable rounds and an 8 character salt. .. 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. Until more user feedback and sample hashes have been gathered, *caveat emptor*. Interface ========= .. autoclass:: sun_md5_crypt() Format ====== An example hash (of ``passwd``) is ``$md5,rounds=5000$GUBv0xjJ$$mSwgIswdjlTY0YxV7HBVm0``. A sun-md5-crypt hash string has the format :samp:`$md5,rounds={rounds}${salt}$${checksum}`, where: * ``$md5,`` is the prefix used to identify the hash. * :samp:`{rounds}` is the decimal number of rounds to use (5000 in the example). * :samp:`{salt}` is 0-8 salt characters drawn from ``[./0-9A-Za-z]`` (``GUBv0xjJ`` in the example). * :samp:`{checksum}` is 22 characters drawn from the same set, encoding a 128-bit checksum (``mSwgIswdjlTY0YxV7HBVm0`` in the example). An alternate format, :samp:`$md5${salt}$${checksum}` is used when the rounds value is 0. There also exists some hashes which have only a single ``$`` between the salt and the checksum; these have a slightly different checksum calculation (see :ref:`smc-bare-salt` for details). .. note:: Solaris seems to deviate from the :ref:`modular-crypt-format` in that it considers ``,`` to indicate the end of the identifier in addition to ``$``. .. rst-class:: html-toggle Algorithm ========= The algorithm used is based around the MD5 message digest and the "Muffett Coin Toss" algorithm. 1. Given a password, the number of rounds, and a salt string. .. _smc-digest-step: 2. an initial MD5 digest is created from the concatenation of the password, and the configuration string (using the format :samp:`$md5,rounds={rounds}${salt}$`, or :samp:`$md5${salt}$` if rounds is 0). (See :ref:`smc-bare-salt` for details about an issue affecting this step) 3. for rounds+4096 iterations, a new digest is created: i. a buffer is initialized, containing the previous round's MD5 digest (for the first round, the digest from step 2 is used). ii. ``MuffetCoinToss(rounds, previous digest)`` is called, resulting in a 0 or 1. iii. If step 3.ii results in a 1, a constant data string is added to the buffer; if the result is a 0, the string is not added for this round. The constant data string is a 1517 byte excerpt from Hamlet [#f2]_ (``To be, or not to be...all my sins remember'd.\n``), including an appended null character. iv. the current iteration as a zero-indexed integer is converted to a string (not zero-padded) and added to the buffer. v. the output for this iteration is the MD5 digest of the buffer's contents. 4. The final digest is then encoded into :mod:`hash64 ` format using the same transposed byte order that :class:`~passlib.hash.md5_crypt` uses, and returned. Muffet Coin Toss ---------------- The Muffet Coin Toss algorithm is as follows: Given the current round number, and a 16 byte MD5 digest, it returns a 0 or 1, using the following formula: .. note:: All references below to a specific bit of the digest should be interpreted mod 128. All references below to a specific byte of the digest should be interpreted mod 16. 1. A 8-bit integer :samp:`{X}` is generated from the following formula: for each :samp:`{i}` in 0..7 inclusive: * let :samp:`{A}` be the :samp:`{i}`'th byte of the digest, as an 8-bit int. * let :samp:`{B}` be the :samp:`{i}+3`'rd byte of the digest, as an 8-bit int. * let :samp:`{R}` be :samp:`{A}` shifted right by :samp:`{B} % 5` bits. * let :samp:`{V}` be the :samp:`{R}`'th byte of the digest. * if the :samp:`{A} % 8`'th bit of :samp:`{B}` is 1, divide :samp:`{V}` by 2. * use the :samp:`{V}`'th bit of the digest as the :samp:`{i}`'th bit of :samp:`{X}`. 2. Another 8-bit integer, :samp:`{Y}`, is generated exactly the same manner as :samp:`{X}`, except that: * :samp:`{A}` is the :samp:`{i}+8`'th byte of the digest, * :samp:`{B}` is the :samp:`{i}+11`'th byte of the digest. 3. if bit :samp:`{round}` of the digest is 1, :samp:`{X}` is divided by 2. 4. if bit :samp:`{round}+64` of the digest is 1, :samp:`{Y}` is divided by 2. 5. the final result is :samp:`{X}`'th bit of the digest XORed against :samp:`{Y}`'th bit of the digest. .. _smc-bare-salt: Bare Salt Issue --------------- According to the only existing documentation of this algorithm [#mct]_, its hashes were supposed to have the format :samp:`$md5${salt}${checksum}`, and include only the bare string :samp:`$md5${salt}` in the salt digest step (see :ref:`step 2 `, above). However, almost all hashes encountered in production environments have the format :samp:`$md5${salt}$${checksum}` (note the double ``$$``). Unfortunately, it is not merely a cosmetic difference: hashes of this format incorporate the first ``$`` after the salt within the salt digest step, so the resulting checksum is different. The documentation hints that this stems from a bug within the production 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 ``$``, e.g. :samp:`$md5${salt}$x`. Passlib attempts to accommodate both formats using the special ``bare_salt`` keyword. It is set to ``True`` to indicate a configuration or hash string which contains only a single ``$``, and does not incorporate it into the hash calculation. The ``$$`` hash is encountered more often in production since it seems the Solaris salt generator always appends a ``$``; because of this ``bare_salt=False`` was chosen as the default, so that hashes will be generated which by default conform to what users are used to. Deviations ========== Passlib's implementation of Sun-MD5-Crypt deliberately deviates from the official implementation in the following ways: * 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 all known reference hashes. In order to provide support for unicode strings, 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. * Rounds encoding 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 (e.g. ``$md5,rounds=0$abc$``). .. _smc-quirks: Given the lack of documentation, lack of test vectors, and known bugs which accompany the original Solaris implementation, Passlib may not accurately be able to generate and verify all hashes encountered in a Solaris environment. Issues of concern include: * Some hashes found on the web use a ``$`` in place of the ``,``. It is unclear whether this is an accepted alternate format or just a typo, nor whether this is supposed to affect the checksum in the resulting hash string. * The current implementation needs addition test vectors; especially ones which contain an explicitly specific number of rounds. * More information is needed about the parsing / formatting issue described in the :ref:`smc-bare-salt` section. .. rubric:: Footnotes .. [#mct] Overview of & motivations for the algorithm - ``_ .. [#f2] The source of Hamlet's speech, used byte-for-byte as the constant data - ``_ passlib-1.6.5/docs/lib/passlib.hash.md5_crypt.rst0000644000175000017500000001715112257351267023041 0ustar biscuitbiscuit00000000000000.. 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 its 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 its origins, it's sometimes referred to as "FreeBSD MD5 Crypt". 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' >>> # verify the password >>> md5_crypt.verify("password", h) True >>> md5_crypt.verify("secret", h) 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() .. 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 ====== An example md5-crypt hash (of the string ``password``) is ``$1$5pZSV9va$azfrPr6af3Fc7dLblQXVa0``. An md5-crypt hash string has the format :samp:`$1${salt}${checksum}`, where: * ``$1$`` is the prefix used to identify md5-crypt hashes, following the :ref:`modular-crypt-format` * :samp:`{salt}` is 0-8 characters drawn from the regexp range ``[./0-9A-Za-z]``; providing a 48-bit salt (``5pZSV9va`` in the example). * :samp:`{checksum}` is 22 characters drawn from the same character set as the salt; encoding a 128-bit checksum (``azfrPr6af3Fc7dLblQXVa0`` in the example). .. _md5-crypt-algorithm: .. rst-class:: html-toggle Algorithm ========= The MD5-Crypt algorithm [#f1]_ calculates a checksum as follows: 1. A password string and salt string are provided. (The salt should not include the magic prefix, it should match the string referred to as :samp:`{salt}` in the format section, above). 2. If needed, the salt should be truncated to a maximum of 8 characters. .. 3. Start MD5 digest B. 4. Add the password to digest B. 5. Add the salt to digest B. 6. Add the password to digest B. 7. Finish MD5 digest B. .. 8. Start MD5 digest A. 9. Add the password 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). 11. Add the salt to digest A. 12. For each block of 16 bytes in the password string, add digest B to digest A. 13. For the remaining N bytes of the password string, add the first N bytes of digest B to digest A. 14. For each bit in the binary representation of the length of the password string; starting with the lowest value bit, up to and including the largest-valued bit that is set to ``1``: a. If the current bit is set 1, add the first character of the password to digest A. b. Otherwise, add a NULL character to digest A. (If the password is the empty string, step 14 is omitted entirely). 15. Finish MD5 digest A. .. 16. For 1000 rounds (round values 0..999 inclusive), perform the following steps: a. Start MD5 Digest C for the round. b. If the round is odd, add the password to digest C. c. If the round is even, add the previous round's result to digest C (for round 0, add digest A instead). d. If the round is not a multiple of 3, add the salt to digest C. e. If the round is not a multiple of 7, add the password to digest C. f. If the round is even, add the password to digest C. g. If the round is odd, add the previous round's result to digest C (for round 0, add digest A instead). h. Use the final value of MD5 digest C as the result for this round. 17. Transpose the 16 bytes of the final round's result in the 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 :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 severely: * It relies on the MD5 message digest, for which theoretical pre-image attacks exist [#f2]_. * More seriously, its 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 its replacement by new algorithms such as :class:`~passlib.hash.bcrypt` and :class:`~passlib.hash.sha512_crypt`. Deviations ========== 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 unambiguously 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 md5-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 md5-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 md5-crypt. If a different encoding is desired by an application, the password should be encoded before handing it to Passlib. .. rubric:: Footnotes .. [#f1] The authoritative reference for MD5-Crypt is Poul-Henning Kamp's original FreeBSD implementation - ``_ .. [#f2] Security issues with MD5 - ``_. .. [#cisco] Note about Cisco Type 5 salt size - ``_. .. [#phk] Deprecation Announcement from Poul-Henning Kamp - ``_. passlib-1.6.5/docs/lib/passlib.context-tutorial.rst0000644000175000017500000005176512257351267023547 0ustar biscuitbiscuit00000000000000.. 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 .. rst-class:: emphasize-children 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: Basic Usage ----------- At its 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: 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: 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 its configuration as a dictionary suitable for passing back into its 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: 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 ... passlib-1.6.5/docs/lib/passlib.hash.mssql2005.rst0000644000175000017500000000552512257351267022603 0ustar biscuitbiscuit00000000000000================================================================== :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 its 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 hexadecimal 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 hexadecimal, 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 its 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 - ``_. passlib-1.6.5/docs/lib/passlib.hash.bsdi_crypt.rst0000644000175000017500000001423012257351267023270 0ustar biscuitbiscuit00000000000000================================================================================= :class:`passlib.hash.bsdi_crypt` - BSDi Crypt ================================================================================= .. 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". It class can be used directly as follows:: >>> from passlib.hash import bsdi_crypt >>> # generate new salt, encrypt password >>> hash = bsdi_crypt.encrypt("password") >>> hash '_7C/.Bf/4gZk10RYRs4Y' >>> # same, but with explict number of rounds >>> bsdi_crypt.encrypt("password", rounds=10001) '_FQ0.amG/zwCMip7DnBk' >>> # verify password >>> bsdi_crypt.verify("password", hash) True >>> bsdi_crypt.verify("secret", hash) False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. 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 ====== An example hash (of the string ``password``) is ``_EQ0.jzhSVeUyoSqLupI``. A bsdi_crypt hash string consists of a 21 character string of the form :samp:`_{rounds}{salt}{checksum}`. 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 :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). A bsdi_crypt configuration string is also accepted by this module; and has the same format as the hash string, but with the checksum portion omitted. .. rst-class:: html-toggle Algorithm ========= The checksum is formed by a modified version of the DES cipher in encrypt mode: 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 :data:`hash64 ` encoding. 3. The 4 character salt string is decoded to a 24-bit integer salt value; 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. 5. The lower 7 bits of the first 8 bytes of the password are used to form a 56-bit integer; with the first byte providing the most significant 7 bits, and the 8th byte providing the least significant 7 bits. This is the DES key. 6. For each additional block of 8 bytes in the padded password: a. The current DES key is encrypted using a single round of normal DES, with itself as the input block. b. Step 5 is repeated for the current 8-byte block, and xored against the existing DES key. 7. Repeated rounds of (modified) DES encryption are performed; starting with a null input block, and using the 56-bit integer from step 5/6 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 number of rounds is controlled by the value decoded in step 2. 8. The 64-bit result of the last round of step 7 is then lsb-padded with 2 zero bits. 9. The resulting 66-bit integer is encoded in big-endian order using the :data:`hash64-big ` format. .. _bsdi-crypt-security-issues: Security Issues =============== BSDi Crypt should not be considered sufficiently secure, for a number of reasons: * Its use of the DES stream cipher, which is vulnerable to practical pre-image attacks, and considered broken, as well as having too-small key and block sizes. * The 24-bit salt is too small to defeat rainbow-table attacks (most modern algorithms provide at least a 48-bit salt). * The fact that it only uses the lower 7 bits of each byte of the password restricts the keyspace which needs to be searched. * 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 mitigated 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 ========== This implementation of bsdi-crypt differs from others in one way: * Unicode Policy: The original bsdi-crypt algorithm was designed for 7-bit ``us-ascii`` encoding only (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`` 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. .. rubric:: Footnotes .. [#] Primary source used for description of bsdi-crypt format & algorithm - ``_ .. [#] Another source describing algorithm - ``_ .. [#weak] DES weak keys - ``_ passlib-1.6.5/docs/lib/passlib.hash.msdcc.rst0000644000175000017500000000636412257351267022230 0ustar biscuitbiscuit00000000000000.. 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 hexadecimal 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 hexadecimal, 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: * Its 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 - ``_ passlib-1.6.5/docs/lib/passlib.hash.django_std.rst0000644000175000017500000001566012555044153023244 0ustar biscuitbiscuit00000000000000.. index:: Django; hash formats ============================================================= :samp:`passlib.hash.django_{digest}` - Django-specific Hashes ============================================================= .. currentmodule:: passlib.hash The `Django `_ web framework provides a module for storing user accounts and passwords (:mod:`!django.contrib.auth`). 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 up to and including Django 1.4 .. seealso:: * :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. .. _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`. These classes can be used directly as follows:: >>> from passlib.hash import django_pbkdf2_sha256 as handler >>> # encrypt password >>> h = handler.encrypt("password") >>> h 'pbkdf2_sha256$10000$s1w0UXDd00XB$+4ORmyvVWAQvoAEWlDgN34vlaJx1ZTZpa1pCSRey2Yk=' >>> # verify password >>> handler.verify("password", h) True >>> handler.verify("eville", h) False .. 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 .. autoclass:: django_bcrypt_sha256() 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 portion 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 >>> handler.verify("eville", h) False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface --------- .. autoclass:: django_salted_md5() .. autoclass:: django_salted_sha1() Format ------ An example :class:`!django_salted_sha1` hash (of ``password``) is: ``sha1$f8793$c4cd18eb02375a037885706d414d68d521ca18c7`` Both of Django's salted hashes have the same basic format, :samp:`{ident}${salt}${checksum}`, where: * :samp:`{ident}` is an identifier (``sha1`` in the case of the example, ``md5`` for :class:`!django_salted_md5`). * :samp:`{salt}` consists of (usually 5) lowercase hexadecimal digits (``f8793`` in the example). * :samp:`{checksum}` is lowercase hexadecimal encoding of the checksum. The checksum is generated by concatenating the salt digits followed by the password, and hashing them using the specified digest (MD5 or SHA-1). The digest is then encoded to hexadecimal. If the password is unicode, it is converted to ``utf-8`` first. Security Issues --------------- Django's salted hashes should not be considered very secure. * They use only a single round of digests with known collision and pre-image attacks (SHA1 & MD5). * While it could be increased, they currently use only 20 bits of entropy in their salt, which is borderline insufficient to defeat rainbow tables. * They digest the encoded hexadecimal salt, not the raw bytes, increasing the odds that a particular salt+password string will be present in a pre-computed tables of ascii digests. Des Crypt Wrapper ================= .. autoclass:: django_des_crypt() Format ------ An example :class:`!django_des_crypt` hash (of ``password``) is ``crypt$cd1a4$cdlRbNJGImptk``; the general format is the same as the salted hashes: :samp:`{ident}${salt}${checksum}`, where: * :samp:`{ident}` is the identifier ``crypt``. * :samp:`{salt}` is 5 lowercase hexadecimal digits (``cd1a4`` in the example). * :samp:`{checksum}` is a :class:`!des_crypt` hash (``cdlRbNJGImptk`` in the example). It should be noted that this class essentially just shoe-horns :class:`des_crypt` into a format compatible with the Django salted hashes (above). It has a few quirks, such as the fact that only the first two characters of the salt are used by :class:`!des_crypt`, and they are in turn duplicated as the first two characters of the checksum. For security issues relating to :class:`!django_des_crypt`, see :class:`des_crypt`. Other Hashes ============ .. autoclass:: django_disabled() .. note:: 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. passlib-1.6.5/docs/lib/passlib.hash.cisco_pix.rst0000644000175000017500000001217412555044153023105 0ustar biscuitbiscuit00000000000000.. 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. .. warning:: This class does not correctly handle hashes generated by Pix/ASA 7.0 (2005) or newer; particularly for passwords 13 characters or more (:issue:`51`). A new :class:`!cisco_asa` will be added in Passlib 1.7 to support these 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 peculiarity. 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: * Its 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. * Its 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. * Its 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 - ``_, ``_ passlib-1.6.5/docs/lib/passlib.utils.pbkdf2.rst0000644000175000017500000000211112555044153022500 0ustar biscuitbiscuit00000000000000============================================================= :mod:`passlib.utils.pbkdf2` - PBKDF2 key derivation algorithm ============================================================= .. module:: passlib.utils.pbkdf2 :synopsis: PBKDF2 and related key derivation algorithms This module provides a couple of key derivation functions, as well as supporting utilities. Primarily, it offers :func:`pbkdf2`, which provides the ability to generate an arbitrary length key using the PBKDF2 key derivation algorithm, as specified in `rfc 2898 `_. This function can be helpful in creating password hashes using schemes which have been based around the pbkdf2 algorithm. PKCS#5 Key Derivation Functions =============================== .. autofunction:: pbkdf1 .. autofunction:: pbkdf2 .. note:: The details of PBKDF1 and PBKDF2 are specified in :rfc:`2898`. Helper Functions ================ .. autofunction:: norm_hash_name .. autofunction:: get_prf .. given how this module is expanding in scope, perhaps it should be renamed "kdf" or "crypto"? passlib-1.6.5/docs/lib/passlib.hash.mysql41.rst0000644000175000017500000000352412257351267022444 0ustar biscuitbiscuit00000000000000.. index:: MySQL; PASSWORD() ===================================================================== :class:`passlib.hash.mysql41` - MySQL 4.1 password hash ===================================================================== .. currentmodule:: passlib.hash .. 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 its 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:: * :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. * :mod:`passlib.apps` for a list of :ref:`premade mysql contexts `. Interface ========= .. autoclass:: mysql41() Format & Algorithm ================== A mysql-41 password hash consists of an asterisk ``*`` followed by 40 hexadecimal 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). The checksum is calculated simply, as the SHA1 hash of the SHA1 hash of the password, which is then encoded into hexadecimal. Security Issues =============== Lacking any sort of salt, and using only 2 rounds of the common SHA1 message digest, it's not very secure, and should not be used for *any* purpose but verifying existing MySQL 4.1+ password hashes. passlib-1.6.5/docs/lib/passlib.hash.rst0000644000175000017500000001702412555044153021125 0ustar biscuitbiscuit00000000000000============================================== :mod:`passlib.hash` - Password Hashing Schemes ============================================== .. module:: passlib.hash :synopsis: all password hashes provided by Passlib Overview ======== The :mod:`!passlib.hash` module contains all the password hash algorithms built into Passlib. While each hash has its own options and output format, they all share a common interface, documented in detail in the :ref:`password-hash-api`. The following pages describe the common interface, and then describe each hash in detail (including its format, underlying 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 For additional details, usage examples, and full documentation of all methods and attributes provided by the common hash interface: .. toctree:: :maxdepth: 2 /password_hash_api .. _mcf-hashes: Unix & "Modular Crypt" Hashes ============================= Aside from the "archaic" schemes below, most modern Unix flavors use password hashes which follow the :ref:`modular crypt format `, allowing them to be easily distinguished when used within the same file. The basic format :samp:`${scheme}${hash}` has also been adopted for use by other applications and password hash schemes. .. _archaic-unix-schemes: Archaic Unix Schemes -------------------- All of the following hashes are/were used by various Unix flavors to store user passwords; most are based on the DES block cipher, and predate the arrival of the modular crypt format. They should all be considered insecure at best, but may be useful when reading legacy password entries: .. toctree:: :maxdepth: 1 passlib.hash.des_crypt passlib.hash.bsdi_crypt passlib.hash.bigcrypt passlib.hash.crypt16 .. _standard-unix-hashes: Standard Unix Schemes --------------------- All these schemes are currently used by various Unix flavors to store user passwords. They all follow the modular crypt format. .. toctree:: :maxdepth: 1 passlib.hash.md5_crypt passlib.hash.bcrypt passlib.hash.sha1_crypt passlib.hash.sun_md5_crypt passlib.hash.sha256_crypt passlib.hash.sha512_crypt Other Modular Crypt Schemes --------------------------- While most of these schemes are not (commonly) used by any Unix flavor to store user passwords, they can be used compatibly along side other modular crypt format hashes. .. toctree:: :maxdepth: 1 passlib.hash.apr_md5_crypt passlib.hash.bcrypt_sha256 passlib.hash.phpass 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" behavior found in many Linux & BSD password files: .. toctree:: :maxdepth: 1 passlib.hash.unix_disabled .. _ldap-hashes: LDAP / RFC2307 Hashes ===================== All of the following hashes use a variant of the password hash format 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 --------------------- .. toctree:: :hidden: passlib.hash.ldap_std The following schemes are explicitly defined by RFC 2307, and are supported by OpenLDAP. * :class:`passlib.hash.ldap_md5` - MD5 digest * :class:`passlib.hash.ldap_sha1` - SHA1 digest * :class:`passlib.hash.ldap_salted_md5` - salted MD5 digest * :class:`passlib.hash.ldap_salted_sha1` - salted SHA1 digest .. toctree:: :maxdepth: 1 passlib.hash.ldap_crypt * :class:`passlib.hash.ldap_plaintext` - LDAP-Aware Plaintext Handler Non-Standard LDAP Schemes ------------------------- None of the following schemes are actually used by LDAP, but follow the LDAP format: .. toctree:: :hidden: passlib.hash.ldap_other * :class:`passlib.hash.ldap_hex_md5` - Hex-encoded MD5 Digest * :class:`passlib.hash.ldap_hex_sha1` - Hex-encoded SHA1 Digest .. toctree:: :maxdepth: 1 passlib.hash.ldap_pbkdf2_digest passlib.hash.atlassian_pbkdf2_sha1 passlib.hash.fshp * :class:`passlib.hash.roundup_plaintext` - Roundup-specific LDAP Plaintext Handler .. _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 not seen outside those specific contexts: .. 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 ============ The following schemes are used in various contexts, but have formats or uses which cannot be easily placed in one of the above categories: .. 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 passlib-1.6.5/docs/lib/passlib.hash.des_crypt.rst0000644000175000017500000001256112257351267023127 0ustar biscuitbiscuit00000000000000======================================================================= :class:`passlib.hash.des_crypt` - DES Crypt ======================================================================= .. warning:: This algorithm is extremely weak by modern standards, and should not be used if possible. .. 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. It can used directly as follows:: >>> from passlib.hash import des_crypt >>> # generate new salt, encrypt password >>> hash = des_crypt.encrypt("password") 'JQMuyS6H.AGMo' >>> # verify the password >>> des_crypt.verify("password", hash) True >>> des_crypt.verify("letmein", hash) False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. 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 :data:`hash64 `-encoded 12 bit integer used as the salt, with the remaining characters forming a hash64-encoded 64-bit integer checksum. A des-crypt configuration string is also accepted by this module, consists of only the first 2 characters, corresponding to the salt only. An example hash (of the string ``password``) is ``JQMuyS6H.AGMo``, where the salt is ``JQ``, and the checksum ``MuyS6H.AGMo``. .. rst-class:: html-toggle Algorithm ========= The checksum is formed by a modified version of the DES cipher in encrypt mode: 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 :data:`hash64 ` encoding. 3. If the password is less than 8 bytes, it's NULL padded at the end to 8 bytes. 4. The lower 7 bits of the first 8 bytes of the password are used to form a 56-bit integer; with the first byte providing the most significant 7 bits, and the 8th byte providing the least significant 7 bits. The remainder of the password (if any) is ignored. 5. 25 repeated rounds of modified DES encryption are performed; 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. Thus, if the salt is set to ``0``, normal DES encryption is performed. (This was intended to prevent optimized implementations of regular DES encryption to be useful in attacking this algorithm). 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 :data:`hash64-big ` format. Security Issues =============== DES-Crypt is no longer considered secure, for a variety of reasons: * Its use of the DES stream cipher, which is vulnerable to practical pre-image attacks, and considered broken, as well as having too-small key and block sizes. * The 12-bit salt is considered to small to defeat rainbow-table attacks (most modern algorithms provide at least a 48-bit salt). * The fact that it only uses the lower 7 bits of the first 8 bytes of the password results in a dangerously small keyspace which needs to be searched. Deviations ========== This implementation of des-crypt differs from others in a few ways: * Minimum salt string: Some implementations of des-crypt permit empty and single-character salt strings. 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 salt string is not at least 2 characters. * Restricted salt string character set: The underlying algorithm expects salt strings to use the :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 string contains any non-standard characters. * Unicode Policy: The original des-crypt algorithm was designed for 7-bit ``us-ascii`` encoding only (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`` 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. .. rubric:: Footnotes .. [#] A java implementation of des-crypt, used as base for Passlib's pure-python implementation, can be found at ``_ passlib-1.6.5/docs/lib/passlib.hash.sha512_crypt.rst0000644000175000017500000000342012257351267023351 0ustar biscuitbiscuit00000000000000=================================================================== :class:`passlib.hash.sha512_crypt` - SHA-512 Crypt =================================================================== .. currentmodule:: passlib.hash 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. .. seealso:: * :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. * :doc:`sha256_crypt ` -- the companion 256-bit version of this hash. Interface ========= .. autoclass:: sha512_crypt() .. note:: This class will use the first available of two possible backends: * stdlib :func:`crypt()`, if the host OS supports SHA512-Crypt (most Linux systems). * a pure python implementation of SHA512-Crypt built into passlib. You can see which backend is in use by calling the :meth:`get_backend()` method. 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. * its 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. passlib-1.6.5/docs/lib/passlib.hash.scram.rst0000644000175000017500000001522212257351267022235 0ustar biscuitbiscuit00000000000000.. 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 documented 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.extract_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 its 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 its own, any pre-image weaknesses 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 - ``_ passlib-1.6.5/docs/lib/passlib.hash.ldap_pbkdf2_digest.rst0000644000175000017500000000262012257351267024635 0ustar biscuitbiscuit00000000000000================================================================= :samp:`passlib.hash.ldap_pbkdf2_{digest}` - Generic PBKDF2 Hashes ================================================================= .. index:: pbkdf2 hash; generic ldap .. currentmodule:: passlib.hash 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. .. seealso:: These classes are simply wrappers around the :doc:`MCF-Compatible Simple PBKDF2 Hashes `. Interface ========= .. class:: ldap_pbkdf2_sha1() this is the same as :class:`pbkdf2_sha1`, except that it uses ``{PBKDF2}`` as its identifying prefix instead of ``$pdkdf2$``. .. class:: ldap_pbkdf2_sha256() this is the same as :class:`pbkdf2_sha256`, except that it uses ``{PBKDF2-SHA256}`` as its identifying prefix instead of ``$pdkdf2-sha256$``. .. class:: ldap_pbkdf2_sha512() this is the same as :class:`pbkdf2_sha512`, except that it uses ``{PBKDF2-SHA512}`` as its identifying prefix instead of ``$pdkdf2-sha512$``. .. rubric:: Footnotes .. [#pbkdf2] The specification for the PBKDF2 algorithm - ``_, part of :rfc:`2898`. passlib-1.6.5/docs/lib/passlib.hash.unix_disabled.rst0000644000175000017500000000277112214647122023736 0ustar biscuitbiscuit00000000000000================================================================== :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 ``*``. passlib-1.6.5/docs/lib/passlib.hosts.rst0000644000175000017500000001151012214647123021332 0ustar biscuitbiscuit00000000000000============================================ :mod:`passlib.hosts` - OS Password Handling ============================================ .. module:: passlib.hosts :synopsis: encrypting & verifying operating system passwords 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`. .. 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 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 for the following Unix variants: .. data:: linux_context context instance which recognizes hashes used by the majority of Linux distributions. encryption defaults to :class:`!sha512_crypt`. .. data:: freebsd_context context instance which recognizes all hashes used by FreeBSD 8. encryption defaults to :class:`!bcrypt`. .. data:: netbsd_context context instance which recognizes all hashes used by NetBSD. encryption defaults to :class:`!bcrypt`. .. data:: openbsd_context context instance which recognizes all hashes used by OpenBSD. encryption defaults to :class:`!bcrypt`. .. note:: 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]_. Current Host OS --------------- .. data:: host_context :platform: Unix This :class:`~passlib.context.CryptContext` instance should detect and support all the algorithms the native OS :func:`!crypt` offers. 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.schemes()``). * it defaults to the strongest algorithm available, automatically configured to an appropriate strength for encrypting new passwords. * whereas :func:`!crypt` typically defaults to using :mod:`~passlib.hash.des_crypt`; and provides little introspection. 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. >>> import spwd, os >>> from passlib.hosts import host_context >>> hash = spwd.getspnam(os.environ['USER']).sp_pwd >>> host_context.verify("toomanysecrets", hash) True .. versionchanged:: 1.4 This object is only available on systems where the stdlib :mod:`!crypt` module is present. In version 1.3 and earlier, it was available on non-Unix systems, though it did nothing useful. .. rubric:: Footnotes .. [#shadow] Man page for Linux /etc/shadow - ``_ passlib-1.6.5/docs/lib/passlib.hash.bcrypt.rst0000644000175000017500000002300312555044153022421 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.bcrypt` - BCrypt ================================================================== .. currentmodule:: passlib.hash BCrypt was developed to replace :class:`~passlib.hash.md5_crypt` for BSD systems. It uses a modified version of the Blowfish stream cipher. Featuring 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. This class can be used directly as follows:: >>> from passlib.hash import bcrypt >>> # generate new salt, encrypt password >>> h = bcrypt.encrypt("password") >>> h '$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy' >>> # the same, but with an explicit number of rounds >>> bcrypt.encrypt("password", rounds=8) '$2a$08$8wmNsdCH.M21f.LSBSnYjQrZ9l1EmtBc9uNPGL.9l75YE8D8FlnZC' >>> # verify password >>> bcrypt.verify("password", h) True >>> bcrypt.verify("wrong", h) False .. note:: It is strongly recommended that you install `bcrypt `_ when using this hash. .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: bcrypt() .. _bcrypt-backends: .. index:: pair: environmental variable; PASSLIB_BUILTIN_BCRYPT Bcrypt Backends --------------- This class will use the first available of five possible backends: 1. `bcrypt `_, if installed. 2. `py-bcrypt `_, if installed. 3. `bcryptor `_, if installed. 4. stdlib's :func:`crypt.crypt()`, if the host OS supports BCrypt (primarily BSD-derived systems). 5. 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()`. As of Passlib 1.6.3, a one-time check is peformed when the backend is first loaded, to detect the backend's capabilities & bugs. If this check detects a fatal bug, a :exc:`~passlib.exc.PasslibSecurityError` will be raised. This generally means you need to upgrade the external package being used as the backend (this will be detailed in the error message). .. warning:: *The pure-python backend (#5) is disabled by default!* That backend is currently too slow to be usable 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 at least 12 cost in under 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 its 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`` Bcrypt hashes have the format :samp:`$2a${rounds}${salt}{checksum}`, where: * :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 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). While BCrypt's basic algorithm is described in its 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. Security Issues =============== .. _bcrypt-password-truncation: * Password Truncation. While not a security issue per-se, bcrypt does have one major limitation: password are truncated on the first NULL byte (if any), and only the first 72 bytes of a password are hashed... all the rest are ignored. Furthermore, bytes 55-72 are not fully mixed into the resulting hash (citation needed!). To work around both these issues, many applications first run the password through a message digest such as SHA2-256. Passlib offers the premade :doc:`passlib.hash.bcrypt_sha256` to take care of this issue. Deviations ========== This implementation of bcrypt differs from others in a few ways: * Restricted salt string character set: 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 situation, Passlib strictly limits salts to the allowed character set, and will throw a :exc:`ValueError` if an invalid salt character is encountered. * 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 bcrypt 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 bcrypt. If a different encoding is desired by an application, the password should be encoded before handing it to Passlib. * Padding Bits BCrypt's base64 encoding results in the last character of the salt 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 :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. .. versionchanged:: 1.6.3 Passlib will now throw a :exc:`~passlib.exc.PasslibSecurityError` if an attempt is made to use any backend which is vulnerable to this bug. * The 'BSD wraparound' bug .. _bsd-wraparound-bug: OpenBSD <= 5.4, and most bcrypt libraries derived from it's source, are vulnerable to a 'wraparound' bug [#wraparound]_, where passwords larger than 254 characters will be incorrectly hashed using only the first few characters of the string, resulting in a severely weakened hash. OpenBSD 5.5 `fixed `_ this flaw, and introduced the ``$2b$`` hash identifier to indicate the hash was generated with the correct algorithm. py-bcrypt <= 0.4 is known to be vulnerable to this, as well as the os_crypt backend (if running on a vulnerable operating system). Passlib 1.6.3 adds the following: * Support for the ``$2b$`` hash format (though for backward compat it has not been made the default yet). * Detects if the active backend is vulnerable to the bug, issues a warning, and enables a workaround so that vulnerable passwords will still be hashed correctly. (This does mean that existing hashes suffering this vulnerability will no longer verify using their correct password). .. rubric:: Footnotes .. [#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 `_ .. [#wraparound] The wraparound flaw is described here - ``_passlib-1.6.5/docs/lib/passlib.utils.md4.rst0000644000175000017500000000173312214647123022023 0ustar biscuitbiscuit00000000000000======================================================== :mod:`passlib.utils.md4` - MD4 message digest algorithm ======================================================== .. module:: passlib.utils.md4 :synopsis: MD4 message digest algorithm .. warning:: This digest is considered **VERY INSECURE**, and not suitable for any new cryptographic activities. Trivial-cost real-world attacks exist for all password algorithms, stream ciphers, etc, that have been based on MD4. Do not use this hash or derived schemes unless you *really* have to. This module implements the MD4 hash algorithm in pure python, based on the `rfc 1320 `_ specification of MD4. .. autoclass:: md4 .. note:: If MD4 support is detected in :mod:`!hashlib`, the :class:`!md4` class in this module will be replaced by a function wrapping :mod:`!hashlib`'s implementation, which should be faster, but otherwise behave exactly the same. passlib-1.6.5/docs/lib/passlib.hash.oracle11.rst0000644000175000017500000000546712257351267022551 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.oracle11` - Oracle 11g password hash ================================================================== .. currentmodule:: passlib.hash This class implements the hash algorithm introduced in version 11g of the Oracle Database. It supersedes the :class:`Oracle 10 ` password hash. This class can be can be used directly as follows:: >>> from passlib.hash import oracle11 as oracle11 >>> # generate new salt, encrypt password >>> hash = oracle11.encrypt("password") >>> hash 'S:4143053633E59B4992A8EA17D2FF542C9EDEB335C886EED9C80450C1B4E6' >>> # verify password >>> oracle11.verify("password", hash) True >>> oracle11.verify("secret", hash) 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 its behavior may not match under various border cases. *caveat emptor*. Interface ========= .. autoclass:: oracle11() Format & Algorithm ================== An example oracle11 hash (of the string ``password``) is: ``S:4143053633E59B4992A8EA17D2FF542C9EDEB335C886EED9C80450C1B4E6`` An oracle11 hash string has the format :samp:`S:{checksum}{salt}`, where: * ``S:`` is the prefix used to identify oracle11 hashes (as distinct from oracle10 hashes, which have no constant prefix). * :samp:`{checksum}` is 40 hexadecimal characters; encoding a 160-bit checksum. (``4143053633E59B4992A8EA17D2FF542C9EDEB335`` in the example) * :samp:`{salt}` is 20 hexadecimal characters; providing a 80-bit salt (``C886EED9C80450C1B4E6`` in the example). The Oracle 11 hash has a very simple algorithm: The salt is decoded from its hexadecimal representation into binary, and the SHA-1 digest of :samp:`{password}{raw_salt}` is then encoded into hexadecimal, and returned as the checksum. Deviations ========== 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: * Unicode Policy 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`` 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. .. rubric:: Footnotes .. [#] Description of Oracle10g and Oracle11g algorithms - ``_. passlib-1.6.5/docs/lib/passlib.utils.handlers.rst0000644000175000017500000002116712257351267023152 0ustar biscuitbiscuit00000000000000.. index:: pair: custom hash handler; implementing ========================================================================== :mod:`passlib.utils.handlers` - Framework for writing password hashes ========================================================================== .. module:: passlib.utils.handlers :synopsis: framework for writing password hashes .. warning:: This module is primarily used as an internal support module. Its 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:: This module, and the instructions on how to write a custom handler, definitely need to be rewritten for clarity. They are not yet organized, and may leave out some important details. 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 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 should look like (if the implementation even uses them). That said, most of the handlers built into Passlib are based around the :class:`GenericHandler` class, and its associated mixin classes. While deriving from this class is not required, doing so will greatly reduce the amount of additional 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 a :class:`CryptContext` constructor, or registered globally with Passlib via the :mod:`passlib.registry` module. .. seealso:: :ref:`testing-hash-handlers` for details about how to test custom handlers against Passlib's unittest suite. The GenericHandler Class ======================== Design ------ 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` which are provided by its related mixin classes. 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 of most of the :ref:`password-hash-api` methods, eliminating the need for almost all the boilerplate associated with writing a password hash. In order to minimize the amount of unneeded features that must be loaded in, the :class:`!GenericHandler` class itself contains only the parts which are needed by almost all handlers: parsing, rendering, and checksum validation. Validation of all other parameters (such as salt, rounds, etc) is split out into separate :ref:`mixin classes ` which enhance :class:`!GenericHandler` with additional features. Usage ----- In order to use :class:`!GenericHandler`, just subclass it, and then do the following: * fill out the :attr:`name` attribute with the name of your hash. * fill out the :attr:`~PasswordHash.setting_kwds` attribute with a tuple listing all the settings your hash accepts. * provide an implementation of the :meth:`from_string` classmethod. this method should take in a potential hash string, parse it into components, and return an instance of the class which contains the parsed components. It should throw a :exc:`ValueError` if no hash, or an invalid hash, is provided. * provide an implementation of the :meth:`to_string` instance method. this method should render an instance of your handler class (such as returned by :meth:`from_string`), returning a hash string. * 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 of the hash, according to the settings (such as salt, etc) stored in the parsed instance this method was called from. note that it should not return the full hash with identifiers, etc; that job should be performed by :meth:`to_string`. Some additional notes: * In addition to simply subclassing :class:`!GenericHandler`, most handlers will also benefit from adding in some of the mixin classes that are designed to add features to :class:`!GenericHandler`. See :ref:`generic-handler-mixins` for more details. * Most implementations will want to alter/override the default :meth:`~GenericHandler.identify` method. By default, it returns ``True`` for all hashes that :meth:`~GenericHandler.from_string` can parse without raising a :exc:`ValueError`; which is reliable, but somewhat slow. For faster identification purposes, subclasses may fill in the :attr:`~GenericHandler.ident` attribute with the hash's identifying prefix, which :meth:`~GenericHandler.identify` will then test for instead of calling :meth:`~GenericHandler.from_string`. For more complex situations, a custom implementation should be used; the :class:`HasManyIdents` mixin may also be helpful. * This class does not support context kwds of any type, since that is a rare enough requirement inside passlib. Interface --------- .. autoclass:: GenericHandler .. _generic-handler-mixins: GenericHandler Mixins --------------------- .. autoclass:: HasSalt .. autoclass:: HasRounds .. autoclass:: HasManyIdents .. autoclass:: HasManyBackends .. autoclass:: HasRawSalt .. autoclass:: HasRawChecksum Examples -------- .. todo:: Show some walk-through examples of how to use GenericHandler and its mixins The StaticHandler class ======================= .. autoclass:: StaticHandler .. todo:: Show some examples of how to use StaticHandler .. index:: pair: custom hash handler; testing Other Constructors ================== .. autoclass:: PrefixWrapper .. _testing-hash-handlers: Testing Hash Handlers ===================== Within its unittests, Passlib provides the :class:`~passlib.tests.utils.HandlerCase` class, which can be subclassed to provide a unittest-compatible test class capable of checking if a handler adheres to the :ref:`password-hash-api`. Usage ----- As an example of how to use :class:`!HandlerCase`, 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... class DesCryptTest(HandlerCase): "test des-crypt algorithm" # [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_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 known_correct_hashes = ( # format: (password, hash) ('', 'OgAwTx2l6NADI'), (' ', '/Hk.VPuwQTXbc'), ('test', 'N1tQbOFcM5fpg'), ('Compl3X AlphaNu3meric', 'um.Wguz3eVCx2'), ('4lpHa N|_|M3r1K W/ Cur5Es: #$%(*)(*%#', 'sNYqfOyauIyic'), ('AlOtBsOl', 'cEpWz5IUCShqM'), (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. # known_unidentified_hashes = [ # bad char in otherwise correctly formatted hash '!gAwTx2l6NADI', ] Interface --------- .. autoclass:: passlib.tests.utils.HandlerCase() passlib-1.6.5/docs/lib/passlib.hash.pbkdf2_digest.rst0000644000175000017500000001041312257351267023634 0ustar biscuitbiscuit00000000000000=============================================================== :samp:`passlib.hash.pbkdf2_{digest}` - Generic PBKDF2 Hashes =============================================================== .. index:: pbkdf2 hash; generic mcf .. currentmodule:: passlib.hash 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` Security-wise, PBKDF2 is currently one of the leading key derivation functions, and has no known security issues. Though the original PBKDF2 specification uses the SHA-1 message digest, it is not vulnerable to any of the known weaknesses of SHA-1 [#hmac-sha1]_, and can be safely used. However, for those still concerned, SHA-256 and SHA-512 versions are offered as well. PBKDF2-SHA512 is one of the three hashes Passlib :ref:`recommends ` for new applications. All of these classes can be used directly as follows:: >>> from passlib.hash import pbkdf2_sha256 >>> # generate new salt, encrypt password >>> hash = pbkdf2_sha256.encrypt("password") >>> hash '$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M' >>> # 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' >>> # verify the password >>> pbkdf2_sha256.verify("password", hash) True >>> pbkdf2_sha256.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 ========= .. autoclass:: pbkdf2_sha256() .. class:: pbkdf2_sha512() except for the choice of message digest, this class is the same as :class:`pbkdf2_sha256`. .. class:: pbkdf2_sha1() except for the choice of message digest, this class is the same as :class:`pbkdf2_sha256`. .. _mcf-pbkdf2-format: Format & Algorithm ================== An example :class:`!pbkdf2_sha256` hash (of ``password``):: $pbkdf2-sha256$6400$.6UI/S.nXIk8jcbdHx3Fhg$98jZicV16ODfEsEZeYPGHU3kbrUrvUEXOPimVSQDD44 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 (``$pbkdf2-sha256$`` in the example). * :samp:`{digest}` - this specifies the particular cryptographic hash used in conjunction with HMAC to form PBKDF2's pseudorandom function for that particular hash (``sha256`` in the example). * :samp:`{rounds}` - the number of iterations that should be performed. 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 ` of the raw salt bytes passed into the PBKDF2 function. * :samp:`{checksum}` - this is the :func:`adapted base64 encoding ` of the raw derived key bytes returned from the PBKDF2 function. Each scheme uses the digest size of its specific hash algorithm (:samp:`{digest}`) as the size of the raw derived key. This is enlarged by approximately 4/3 by the base64 encoding, 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 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.ab64_encode`. .. rubric:: Footnotes .. [#pbkdf2] The specification for the PBKDF2 algorithm - ``_, part of :rfc:`2898`. .. [#hmac-sha1] While SHA1 has fallen to collision attacks, HMAC-SHA1 as used by PBKDF2 is still considered secure - ``_. passlib-1.6.5/docs/lib/passlib.hash.ldap_crypt.rst0000644000175000017500000000461412214647123023264 0ustar biscuitbiscuit00000000000000================================================================ :samp:`passlib.hash.ldap_{crypt}` - LDAP crypt() Wrappers ================================================================ .. currentmodule:: passlib.hash Passlib provides support for all the standard LDAP hash formats specified by :rfc:`2307`. One of these, identified by RFC 2307 as the ``{CRYPT}`` scheme, 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_{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:: >>> from passlib.hash import ldap_md5_crypt >>> # encrypt password >>> hash = ldap_md5_crypt.encrypt("password") >>> hash '{CRYPT}$1$gwvn5BO0$3dyk8j.UTcsNUPrLMsU6/0' >>> # verify password >>> ldap_md5_crypt.verify("password", hash) True >>> ldap_md5_crypt.verify("secret", hash) False >>> # 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" .. seealso:: * :ref:`password hash usage ` -- for more usage examples * :doc:`ldap_{digest} ` -- for the other standard LDAP hashes. * :mod:`passlib.apps` -- for a list of :ref:`premade ldap contexts `. Interface ========= .. class:: ldap_des_crypt() .. class:: ldap_bsdi_crypt() .. class:: ldap_md5_crypt() .. class:: ldap_bcrypt() .. class:: ldap_sha1_crypt() .. class:: ldap_sha256_crypt() .. class:: ldap_sha512_crypt() All of these classes have the same interface as their corresponding underlying hash (e.g. :class:`des_crypt`, :class:`md5_crypt`, etc). .. rubric:: Footnotes .. [#pwd] The manpage for :command:`slappasswd` - ``_. .. [#rfc] The basic format for these hashes is laid out in RFC 2307 - ``_ passlib-1.6.5/docs/lib/passlib.hash.hex_digests.rst0000644000175000017500000000406412257351267023440 0ustar biscuitbiscuit00000000000000=============================================================== :samp:`passlib.hash.hex_{digest}` - Generic Hexadecimal Digests =============================================================== .. warning:: 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 Some existing applications store passwords by storing them using hexadecimal-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 can be used directly as follows:: >>> from passlib.hash import hex_sha1 as hex_sha1 >>> # encrypt password >>> h = hex_sha1.encrypt("password") >>> h '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' >>> # verify correct password >>> hex_sha1.verify("password", h) True >>> # verify incorrect password >>> hex_sha1.verify("secret", h) False .. seealso:: the generic :ref:`PasswordHash usage examples ` .. index:: virtualbox; passwordhash Interface ========= .. class:: hex_md4() .. class:: hex_md5() .. class:: hex_sha1() .. class:: hex_sha256() .. class:: hex_sha512() Each of these classes implements a plain hexadecimal 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 ================== All of these classes just report the result of the specified digest, encoded as a series of lowercase hexadecimal characters; though upper case is accepted as input. passlib-1.6.5/docs/lib/passlib.hash.ldap_std.rst0000644000175000017500000001014412257641470022716 0ustar biscuitbiscuit00000000000000============================================================= :samp:`passlib.hash.ldap_{digest}` - RFC2307 Standard Digests ============================================================= .. currentmodule:: passlib.hash Passlib provides support for all the standard LDAP hash formats specified by :rfc:`2307`. This includes ``{MD5}``, ``{SMD5}``, ``{SHA}``, ``{SSHA}``. These schemes range from somewhat to very insecure, and should not be used except when required. These classes all wrap the underlying hashlib implementations, and are can be used directly as follows:: >>> from passlib.hash import ldap_salted_md5 as lsm >>> # encrypt password >>> hash = lsm.encrypt("password") >>> hash '{SMD5}OqsUXNHIhHbznxrqHoIM+ZT8DmE=' >>> # verify password >>> lms.verify("password", hash) True >>> lms.verify("secret", hash) 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 ============ .. warning:: These hashes should not be considered secure in any way, as they are nothing but raw MD5 & SHA-1 digests, which are extremely vulnerable to brute-force attacks. .. autoclass:: ldap_md5() .. autoclass:: ldap_sha1() Format ------ These hashes have the format :samp:`{prefix}{checksum}`. * :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. An example ldap_md5 hash (of ``password``) is ``{MD5}X03MO1qnZdYdgyfeuILPmQ==``. An example ldap_sha1 hash (of ``password``) is ``{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=``. Salted Hashes ============= .. autoclass:: ldap_salted_md5() .. autoclass:: ldap_salted_sha1() These hashes have the format :samp:`{prefix}{data}`. * :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 multi-byte binary salt, and :samp:`{checksum}` is the raw digest of the the string :samp:`{password}{salt}`, using the appropriate digest algorithm. Format ------ An example hash (of ``password``) is ``{SMD5}jNoSMNY0cybfuBWiaGlFw3Mfi/U=``. After decoding, this results in a raw salt string ``s\x1f\x8b\xf5``, and a raw MD5 checksum of ``\x8c\xda\x120\xd64s&\xdf\xb8\x15\xa2hiE\xc3``. An example hash (of ``password``) is ``{SSHA}pKqkNr1tq3wtQqk+UcPyA3HnA2NsU5NJ``. After decoding, this results in a raw salt string ``lS\x93I``, and a raw SHA1 checksum of ``\xa4\xaa\xa46\xbdm\xab|-B\xa9>Q\xc3\xf2\x03q\xe7\x03c``. Security Issues --------------- The LDAP salted hashes should not be considered very secure. * They use only a single round of digests with known collision and pre-image attacks (SHA1 & MD5). * They currently use only 32 bits of entropy in their salt, which is only borderline sufficient to defeat rainbow tables, and cannot (portably) be increased. Plaintext ========= .. autoclass:: ldap_plaintext() 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 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 - ``_ passlib-1.6.5/docs/lib/passlib.ext.django.rst0000644000175000017500000001546112555044153022246 0ustar biscuitbiscuit00000000000000.. index:: Django; password hashing plugin .. module:: passlib.ext.django ========================================================== :mod:`passlib.ext.django` - Django Password Hashing Plugin ========================================================== .. versionadded:: 1.6 .. note:: Passlib 1.7's :mod:`passlib.ext.django` extension will drop support for Django 1.5 and earlier, and require Django 1.6 or newer. This module contains a `Django `_ plugin which overrides all of Django's password hashing functions, replacing them with wrappers around a Passlib :ref:`CryptContext ` object whose configuration is controlled 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. .. note:: 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: 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 ============= 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_CONFIG`` This option specifies the CryptContext configuration options that will be used when the plugin is loaded. * Its 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 its 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:: 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 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 overriding 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. ``PASSLIB_CONTEXT`` .. 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. .. module:: passlib.ext.django.utils .. autofunction:: get_preset_config .. data:: PASSLIB_DEFAULT This constant contains the default configuration for ``PASSLIB_CONFIG``. It provides the following features: * 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. As of Passlib 1.6, it contains the following string:: [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 = django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5, sha512_crypt, bcrypt, phpass ; 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 = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5 ; 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 passlib-1.6.5/docs/lib/passlib.hash.bcrypt_sha256.rst0000644000175000017500000000566212553722451023526 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.bcrypt_sha256` - BCrypt+SHA256 ================================================================== .. versionadded:: 1.6.2 .. currentmodule:: passlib.hash BCrypt was developed to replace :class:`~passlib.hash.md5_crypt` for BSD systems. It uses a modified version of the Blowfish stream cipher. It does, however, truncate passwords to 72 bytes, and some other minor quirks (see :ref:`BCrypt Password Truncation ` for details). This class works around that issue by first running the password through SHA2-256. This class can be used directly as follows:: >>> from passlib.hash import bcrypt_sha256 >>> # generate new salt, encrypt password >>> h = bcrypt_sha256.encrypt("password") >>> h '$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO' >>> # the same, but with an explicit number of rounds >>> bcrypt.encrypt("password", rounds=8) '$bcrypt-sha256$2a,8$UE3dIZ.0I6XZtA/LdMrrle$Ag04/5zYu./12.OSqInXZnJ.WZoh1ua' >>> # verify password >>> bcrypt.verify("password", h) True >>> bcrypt.verify("wrong", h) False .. note:: It is strongly recommended that you install `bcrypt `_ when using this hash. See :doc:`passlib.hash.bcrypt` for more details. Interface ========= .. autoclass:: bcrypt_sha256() Format ====== Bcrypt-SHA256 is compatible with the :ref:`modular-crypt-format`, and uses ``$bcrypt-sha256$`` as the identifying prefix for all it's strings. An example hash (of ``password``) is: ``$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO`` Bcrypt-SHA256 hashes have the format :samp:`$bcrypt-sha256${variant},{rounds}${salt}${checksum}`, where: * :samp:`{variant}` is the BCrypt variant in use (usually, as in this case, ``2a``). * :samp:`{rounds}` is a cost parameter, encoded as decimal integer, which determines the number of iterations used via :samp:`{iterations}=2**{rounds}` (rounds is 12 in the example). * :samp:`{salt}` is a 22 character salt string, using the characters in the regexp range ``[./A-Za-z0-9]`` (``LrmaIX5x4TRtAwEfwJZa1.`` in the example). * :samp:`{checksum}` is a 31 character checksum, using the same characters as the salt (``2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO`` in the example). Algorithm ========= The algorithm this hash uses is as follows: * first the password is encoded to ``UTF-8`` if not already encoded. * then it's run through SHA2-256 to generate a 32 byte digest. * this is encoded using base64, resulting in a 44-byte result (including the trailing padding ``=``). For the example ``"password"``, the output from this stage would be ``"XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg="``. * this base64 string is then passed on to the underlying bcrypt algorithm as the new password to be hashed. See :doc:`passlib.hash.bcrypt` for details on it's operation. passlib-1.6.5/docs/lib/passlib.hash.cisco_type7.rst0000644000175000017500000001104412257351267023356 0ustar biscuitbiscuit00000000000000.. 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 hexadecimal 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 hexadecimal, 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 - ``_, ``_ passlib-1.6.5/docs/lib/passlib.context.rst0000644000175000017500000004331012555044153021663 0ustar biscuitbiscuit00000000000000.. index:: CryptContext; reference .. currentmodule:: passlib.context .. _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. .. rst-class:: emphasize-children The CryptContext Class ====================== .. class:: CryptContext(schemes=None, \*\*kwds) Helper for encrypting passwords using different algorithms. At its 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. * `Configuration Errors`_ -- overview of errors that may be thrown by :class:`!CryptContext` constructor .. index:: CryptContext; keyword options .. rst-class:: html-toggle expanded 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 retrieve 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. .. versionadded:: 1.6 Added support for the ``["auto"]`` value. .. 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 Primary Methods --------------- The main interface to the CryptContext object deliberately mirrors the :ref:`PasswordHash ` interface, since its 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 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 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 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 Examining the Configuration --------------------------- The CryptContext object also supports basic inspection of its current configuration: .. automethod:: CryptContext.schemes .. automethod:: CryptContext.default_scheme .. automethod:: CryptContext.handler .. rst-class:: html-toggle expanded 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 Configuration Errors -------------------- The following errors may be raised when creating a :class:`!CryptContext` instance via any of its constructors, or when updating the configuration of an existing instance: :raises ValueError: * If a configuration option contains an invalid value (e.g. ``all__vary_rounds=-1``). * If the configuration contains valid but incompatible options (e.g. listing a scheme as both :ref:`default ` and :ref:`deprecated `). :raises KeyError: * If the configuration contains an unknown or forbidden option (e.g. :samp:`{scheme}__salt`). * If the :ref:`schemes `, :ref:`default `, or :ref:`deprecated ` options reference an unknown hash scheme (e.g. ``schemes=['xxx']``) :raises TypeError: * If a configuration value has the wrong type (e.g. ``schemes=123``). Note that this error shouldn't occur when loading configurations from a file/string (e.g. using :meth:`CryptContext.from_string`). Additionally, a :exc:`~passlib.exc.PasslibConfigWarning` may be issued if any invalid-but-correctable values are encountered (e.g. if :samp:`sha256_crypt__min_rounds` is set to less than :class:`~passlib.hash.sha256_crypt` 's minimum of 1000). .. versionchanged:: 1.6 Previous releases used Python's builtin :exc:`UserWarning` instead of the more specific :exc:`!passlib.exc.PasslibConfigWarning`. Other Helpers ============= .. autoclass:: LazyCryptContext([schemes=None,] \*\*kwds [, onload=None]) .. rst-class:: html-toggle The CryptPolicy Class (deprecated) ================================== .. autoclass:: CryptPolicy passlib-1.6.5/docs/lib/passlib.hash.plaintext.rst0000644000175000017500000000216612214647123023133 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.plaintext` - Plaintext ================================================================== .. currentmodule:: passlib.hash 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. 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 plaintext >>> # "encrypt" password >>> plaintext.encrypt("password") 'password' >>> # verify password >>> plaintext.verify("password", "password") True >>> 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() passlib-1.6.5/docs/lib/passlib.hash.crypt16.rst0000644000175000017500000001074512257351267022445 0ustar biscuitbiscuit00000000000000======================================================================= :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. .. seealso:: :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. Interface ========= .. 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 :data:`hash64 `-encoded 12-bit integer (``aa`` in the example). * each :samp:`{checksum_i}` is a separate checksum, stored as an 11 character :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 Algorithm ========= The crypt16 algorithm uses a weakened version of the des-crypt algorithm: 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 :data:`hash64 ` encoding. 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 the most significant 7 bits, and the 8th character providing the least significant 7 bits. 5. 20 repeated rounds of modified DES encryption are performed; starting with a null input block, and using the 56-bit integer from step 4 as the DES key. 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 :data:`hash64-big ` format. This is the first checksum segment. 8. The second checksum segment is created by repeating steps 4..7 using the second 8 bytes of the padding password (from step 3). The only difference is that step 5 uses only 5 rounds. 9. The final checksum string is the concatenation of the two checksum segments, in order. Security Issues =============== Crypt16 is dangerously flawed: * It suffers from all the flaws of :class:`~passlib.hash.des_crypt`. * Compared to des-crypt, its smaller number of rounds makes it even *more* vulnerable to brute-force attacks. * For a given salt, passwords under 9 characters all have the same 2nd checksum. Given the 12-bit salt size, all such 2nd checksums can be easily pre-computed; making an attack easier, and giving away information about password size. * Since both checksums use the same salt, they can be attacked at once (by doing 5 rounds, checking the result against checksum 2, doing 15 rounds more, and checking the result against checksum 1). Deviations ========== This implementation of crypt16 deviates from public documentation of the format in one way: * Unicode Policy: The original crypt16 algorithm was designed for 7-bit ``us-ascii`` encoding only (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`` before running them through crypt16. If a different encoding is desired by an application, the password should be encoded before handing it to Passlib. .. rubric:: Footnotes .. [#] One source of information about bigcrypt & crypt16 - ``_ passlib-1.6.5/docs/lib/passlib.registry.rst0000644000175000017500000000537612257351267022067 0ustar biscuitbiscuit00000000000000=================================================== :mod:`passlib.registry` - Password Handler Registry =================================================== .. module:: passlib.registry :synopsis: registry for tracking 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. .. warning:: This module is primarily used as an internal support module. Its 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 ========= .. autofunction:: get_crypt_handler(name[, default]) .. autofunction:: list_crypt_handlers .. autofunction:: register_crypt_handler_path .. autofunction:: register_crypt_handler(handler, force=False) .. note:: All password hashes registered with passlib can be imported by name from the :mod:`passlib.hash` module. This is true not just of the built-in hashes, but for any hash registered with the registration functions in this module. Usage ===== Example showing how to use :func:`!registry_crypt_handler_path`:: >>> # 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, its 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" >>> from passlib.context import CryptContext >>> cc = CryptContext(schemes=["myhash"]) #<-- this will cause autoimport Example showing how to load a hash by name:: >>> from passlib.registry import get_crypt_handler >>> get_crypt_handler("sha512_crypt") >>> get_crypt_handler("missing_hash") KeyError: "no crypt handler found for algorithm: 'missing_hash'" >>> get_crypt_handler("missing_hash", None) None passlib-1.6.5/docs/lib/passlib.hash.fshp.rst0000644000175000017500000001160412257351267022070 0ustar biscuitbiscuit00000000000000========================================================== :class:`passlib.hash.fshp` - Fairly Secure Hashed Password ========================================================== .. index:: fshp .. note:: While the SHA-2 variants of PBKDF1 have no critical security vulnerabilities, PBKDF1 itself has been deprecated in favor of its successor, PBKDF2. Furthermore, FSHP has been listed as insecure by its author (for unspecified reasons); so this scheme should probably only be used to support existing hashes. .. 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. 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 >>> hash = fshp.encrypt("password") >>> hash '{FSHP1|16|16384}PtoqcGUetmVEy/uR8715TNqKa8+teMF9qZO1lA9lJNUm1EQBLPZ+qPRLeEPHqy6C' >>> # 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/rmNW pgZcZllZbCra5GJ8ZfFRNwCHirPqvYTAnbaQQeFQbWym/frRrRev3buoygFQRYexl4091Pc5m' >>> # verify password >>> fshp.verify("password", hash) True >>> fshp.verify("secret", hash) False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: fshp() Format & Algorithm ================== 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:`{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:`{saltsize}` is a decimal integer identifying the number of bytes in the salt. ``16`` in the example. * :samp:`{rounds}` is a decimal integer identifying the number of rounds to apply when calculating the checksum (see below). ``16384`` in the example. * :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 hexadecimal octets) of: ``3eda2a70651eb66544cbfb91f3bd794c`` and a checksum value (in hexadecimal octets) of: ``da8a6bcfad78c17da993b5940f6524d526d444012cf67ea8f44b7843c7ab2e82`` FSHP is basically just a wrapper around PBKDF1: The checksum is calculated using :func:`~passlib.utils.pbkdf2.pbkdf1`, passing in the password, the decoded salt string, the number of rounds, and hash function specified by the variant identifier. FSHP has one quirk in that the password is passed in as the pbkdf1 salt, and the salt is passed in as the pbkdf1 password. Security Issues =============== * A minor issue is that FSHP swaps the location the password and salt from what is described in the PBKDF1 standard. This issue is mainly noted in order to dismiss it: while the swap permits an attacker to pre-calculate part of the initial digest, the impact of this is negligible when a large number of rounds is used. * Since PBKDF1 is based on repeated composition of a hash, it is vulnerable to any first-preimage attacks on the underlying hash. This has led to the deprecation of using SHA-1 or earlier hashes with PBKDF1. In contrast, its successor PBKDF2 was designed to mitigate this weakness (among other things), and enjoys much stronger preimage resistance when used with the same cryptographic hashes. Deviations ========== * Unicode Policy: The official FSHP python implementation takes in a password specified as a series of bytes, and does not specify what encoding should be used; though a ``us-ascii`` compatible encoding is implied by the implementation, 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 FSHP. If a different encoding is desired by an application, the password should be encoded before handing it to Passlib. .. rubric:: Footnotes .. [#home] The FSHP homepage contains implementations in a wide variety of programming languages -- ``_. .. [#pbk] rfc defining PBKDF1 & PBKDF2 - ``_ - passlib-1.6.5/docs/lib/passlib.hash.lmhash.rst0000644000175000017500000001475612257351267022417 0ustar biscuitbiscuit00000000000000.. 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 its 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 hexadecimal 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 using the "OEM Codepage" of the Windows release that the host / target server is running [#cp]_. For pure-ASCII passwords, this step can be performed using the ``us-ascii`` encoding (as most OEM Codepages are ASCII-compatible). However, 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 to 14 bytes, or the end NULL padded to 14 bytes; as appropriate. 3. The first 7 bytes of the truncated password from 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 3 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 hexadecimal. Security Issues =============== Due to a myriad of flaws, and the existence high-speed password cracking software dedicated to LMHASH, this algorithm should be considered broken. The major flaws include: * 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 independent chunks, they can be attacked independently and simultaneously. * The independence 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. * 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 non-ASCII characters to hash in a manner incompatible with the canonical MS Windows implementation. Thus if an application wishes to provide support for non-ASCII passwords, it must decide which encoding to use. 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: .. note:: Future releases of Passlib may change this behavior as new information and code is integrated. Once critical step in the LMHASH algorithm is converting the password to upper case. While ASCII characters are uppercased as normal, non-ASCII characters are converted in implementation-dependant ways: Windows systems encode the password first, and then convert it to uppercase using an codepage-specific table. For the most part these tables seem 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]_). In contrast, most third-party implementations (Passlib included) perform the uppercase conversion first using the Unicode specification, and then encode the password second; despite the non-ASCII border cases where the resulting 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 - ``_. passlib-1.6.5/docs/lib/passlib.hash.grub_pbkdf2_sha512.rst0000644000175000017500000000622712257351267024407 0ustar biscuitbiscuit00000000000000============================================================= :class:`passlib.hash.grub_pbkdf2_sha512` - Grub's PBKDF2 Hash ============================================================= .. index:: pbkdf2 hash; grub .. currentmodule:: passlib.hash This class provides an implementation of Grub's PBKDF2-HMAC-SHA512 password hash [#grub]_, as generated by the :command:`grub-mkpasswd-pbkdf2` command, and may be found in Grub2 configuration files. PBKDF2 is a key derivation function [#pbkdf2]_ that is ideally suited as the basis for a password hash, as it provides variable length salts, variable number of rounds. .. seealso:: * :ref:`password hash usage ` -- for examples of how to use this class via the common hash interface. * :doc:`passlib.hash.pbkdf2_{digest} ` -- for some other PBKDF2-based hashes. Interface ========= .. autoclass:: grub_pbkdf2_sha512() Format & Algorithm ================== A example hash (of ``password``) is :: grub.pbkdf2.sha512.10000.4483972AD2C52E1F590B3E2260795FDA9CA0B07B 96FF492814CA9775F08C4B59CD1707F10B269E09B61B1E2D11729BCA8D62B7827 B25B093EC58C4C1EAC23137.DF4FCB5DD91340D6D31E33423E4210AD47C7A4DF9 FA16F401663BF288C20BF973530866178FE6D134256E4DBEFBD984B652332EED3 ACAED834FEA7B73CAE851D All of this scheme's hashes have the format :samp:`grub.pbkdf2.sha512.{rounds}.{salt}.{checksum}`, where :samp:`{rounds}` is the number of iteration stored in decimal, :samp:`{salt}` is the salt string encoded using upper-case hexadecimal, and :samp:`{checksum}` is the resulting 64-byte derived key, also encoded in upper-case hexadecimal. It can be identified by the prefix ``grub.pdkdf2.sha512.``. The algorithm used is the same as :class:`pbkdf2_sha1`: the password is encoded into UTF-8 if not already encoded, and passed through :func:`~passlib.utils.pbkdf2.pbkdf2` along with the decoded salt, and the number of rounds. The result is then encoded into hexadecimal. .. Hash Translation ---------------- Note that despite encoding and format differences, :class:`pbkdf2_sha512` and :class:`!grub_pbkdf2_sha512` share an identical algorithm, and one can be converted to the other using the following code:: >>> from passlib.hash import pbkdf2_sha512, grub_pbkdf2_sha512 >>> # 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 >>> hobj = pbkdf2_sha512.from_string(h) >>> rounds, salt, chk = hobj.rounds, hobj.salt, hobj.checksum >>> # and a new grub hash can be created >>> gobj = grub_pbkdf2_sha512(rounds=rounds, salt=salt, checksum=chk) >>> g = gobj.to_string() >>> g >>> grub_pbkdf2_sha512.verify("password", g) True .. rubric:: Footnotes .. [#grub] Information about Grub's password hashes - ``_. .. [#pbkdf2] The specification for the PBKDF2 algorithm - ``_. passlib-1.6.5/docs/lib/passlib.utils.des.rst0000644000175000017500000000150212214647123022104 0ustar biscuitbiscuit00000000000000============================================= :mod:`passlib.utils.des` - DES routines ============================================= .. module:: passlib.utils.des :synopsis: routines for performing DES encryption .. warning:: NIST has declared DES to be "inadequate" for cryptographic purposes. These routines, and the password hashes based on them, should not be used in new applications. This module contains routines for encrypting blocks of data using the DES algorithm. 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:: des_encrypt_int_block passlib-1.6.5/docs/requirements.txt0000644000175000017500000000006012555044153020522 0ustar biscuitbiscuit00000000000000hg+https://bitbucket.org/ecollins/cloud_sptheme passlib-1.6.5/docs/install.rst0000644000175000017500000000710312555044153017443 0ustar biscuitbiscuit00000000000000============ Installation ============ .. index:: Google App Engine; compatibility Supported Platforms =================== Passlib requires Python 2 (>= 2.5) or Python 3. It is known to work with the following Python implementations: * CPython 2 -- v2.5 or newer. * CPython 3 -- all versions. * PyPy -- v1.5 or newer. * PyPy3 -- v2.1 or newer. * Jython -- v2.5 or newer. Passlib should work with all operating systems and environments, as it contains builtin fallbacks for almost all OS-dependant features. Google App Engine is supported as well. .. warning:: **Passlib 1.7 will drop support for Python 2.5, 3.0, and 3.1**; and will require Python 2.6 / 3.2 or newer. .. _optional-libraries: Optional Libraries ================== * `bcrypt `_, `py-bcrypt `_, or `bcryptor `_ If any of these packages are installed, they will be used to provide support for the BCrypt hash algorithm. This is required if you want to handle BCrypt hashes, and your OS does not provide native BCrypt support via stdlib's :mod:`!crypt` (which includes pretty much all non-BSD systems). `bcrypt `_ is currently the recommended option -- it's actively maintained, and compatible with both CPython and PyPy. * `M2Crypto `_ If installed, M2Crypto will be used to accelerate some internal functions used by some PBKDF2-based hashes, but it is not required even in that case. Installation Instructions ========================= To install from PyPi using :command:`pip`:: pip install passlib To install from the source using :command:`setup.py`:: python setup.py install .. index:: pair: environmental variable; PASSLIB_TEST_MODE .. rst-class:: html-toggle Testing ======= 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 main suite of tests may be run from the source directory:: nosetests --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:: PASSLIB_TEST_MODE="full" nosetests --tests passlib/tests Tests may also be run via ``setup.py test`` or the included ``tox.ini`` file. .. rst-class:: html-toggle 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.3 or newer) 2. Install the `Cloud Sphinx Theme `_ (1.7 or newer). 3. Download the Passlib source 4. From the Passlib source directory, run :samp:`python setup.py build_sphinx`. 5. Once Sphinx completes its 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`. passlib-1.6.5/docs/password_hash_api.rst0000644000175000017500000007172512555044153021506 0ustar biscuitbiscuit00000000000000.. index:: single: PasswordHash interface single: custom hash handler; requirements .. module:: passlib.ifc :synopsis: abstract interfaces used by Passlib .. _password-hash-api: ============================================= Password Hash Interface ============================================= Overview ======== 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: .. class:: PasswordHash() 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: * :meth:`~PasswordHash.encrypt` - generate new salt, return hash of password. * :meth:`~PasswordHash.verify` - verify password against existing hash. While not needed by most applications, the following methods provide an interface that mimics the traditional Unix :func:`crypt` function: * :meth:`~PasswordHash.genconfig` - create configuration string from salt & other options. * :meth:`~PasswordHash.genhash` - hash password using existing hash or configuration string. One additional support method is provided: * :meth:`~PasswordHash.identify` - check if hash belongs to this algorithm. Each hash algorithm also provides a number of :ref:`informational attributes `, allowing programmatic inspection of its options and parameter limits. .. _password-hash-examples: 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:: >>> # 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' >>> # 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' >>> # 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 .. note:: Whether a hash supports a particular configuration keyword (such as ``rounds``) can be determined from its documentation page; but also programmatically from its :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 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 >>> # 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 >>> # 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 its documentation page; but also programmatically from its :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, the returned string will contain: an algorithm identifier, a cost parameter, the salt string, and finally the password digest itself. :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``. :returns: Resulting password hash, encoded in an algorithm-specific format. This will always be an instance of :class:`!str` (i.e. :class:`unicode` under Python 3, ``ascii``-encoded :class:`bytes` under Python 2). :raises ValueError: * If a ``kwd``'s value is invalid (e.g. if a ``salt`` string is too small, or a ``rounds`` value is out of range). * If ``secret`` contains characters forbidden by the hash algorithm (e.g. :class:`!des_crypt` forbids NULL characters). :raises TypeError: * if ``secret`` is not :class:`!unicode` or :class:`bytes`. * if a ``kwd`` argument has an incorrect type. * if an algorithm-specific required ``kwd`` is not provided. *(Note that the name of this method is a misnomer: nearly all password hashes use an irreversible cryptographic digest, rather than a reversible cipher. 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). .. classmethod:: PasswordHash.verify(secret, hash, \*\*context_kwds) Verify a secret using an existing hash. This checks if a secret matches against the one stored inside the specified hash. :type secret: unicode or bytes :param secret: A string containing the password to check. :type secret: unicode or bytes :param hash: A string containing the hash to check against, such as returned by :meth:`~encrypt`. Hashes may be specified as :class:`!unicode` or ``ascii``-encoded :class:`!bytes`. :param \*\*kwds: Very few hashes will have additional keywords. 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: ``True`` if the secret matches, otherwise ``False``. :raises TypeError: * if either ``secret`` or ``hash`` is not a unicode or bytes instance. * if the hash requires additional ``kwds`` which are not provided, * if a ``kwd`` argument has 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 ``None`` or a config string is provided instead of a properly-formed hash; previous releases were inconsistent in their handling of these two border cases. .. _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 its 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``. :returns: A configuration string (as :class:`!str`), or ``None`` if the scheme does not support a separate configuration. :raises ValueError, TypeError: This function raises exceptions for the same reasons as :meth:`~PasswordHash.encrypt`. .. 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: string containing the password to be encrypted. :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`. ``None`` is accepted *only* for the hashes which lack a configuration string (for which :meth:`~PasswordHash.genconfig` always returns ``None``). :param \*\*kwds: Very few hashes will have additional keywords. 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: 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) 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 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:: A small number of the hashes supported by Passlib lack a reliable method of identification (e.g. :class:`~passlib.hash.lmhash` and :class:`~passlib.hash.nthash` both consist of 32 hexadecimal characters, with no distinguishing features). For such hashes, this method may return false positives. .. seealso:: If you are considering using this method to select from multiple algorithms (e.g. in order to verify a password), you will be better served by the :ref:`CryptContext ` class. .. the undocumented and experimental support methods currently include parsehash() and bitsize() .. _informational-attributes: Informational Attributes ======================== .. _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: .. attribute:: PasswordHash.name Name uniquely identifying this hash. 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. This should always be a :class:`!str` consisting of lowercase ``a-z``, the digits ``0-9``, and the underscore character ``_``. .. attribute:: PasswordHash.setting_kwds 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: .. index:: single: salt; PasswordHash keyword ``salt`` Specifies a fixed salt string to use, rather than randomly generating one. 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. .. index:: single: salt_size; PasswordHash keyword ``salt_size`` 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. .. seealso:: the :ref:`salt info ` attributes (below) .. index:: single: rounds; PasswordHash keyword ``rounds`` If present, this means the hash can vary the number of internal rounds used in some part of its algorithm, allowing the calculation to take a variable amount of processor time, for increased security. 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 an invalid value to :meth:`~PasswordHash.encrypt` will result in a :exc:`ValueError`. However, if ``relaxed=True`` then 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 are not limited to): ``rounds`` and ``salt_size`` values that are too low or too high, ``salt`` strings that are too large. This option is supported by most of the hashes in Passlib. .. versionadded:: 1.6 .. 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 ``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 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). .. attribute:: PasswordHash.min_salt_size The minimum number of bytes/characters required for the salt. Must be an integer between 0 and :attr:`~PasswordHash.max_salt_size`. .. attribute:: PasswordHash.default_salt_size 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 time-cost parameter, ``"rounds"`` should be listed in their :attr:`~PasswordHash.setting_kwds`, and the following attributes should be defined: .. attribute:: PasswordHash.max_rounds The maximum number of rounds the scheme allows. Specifying a value beyond this will result in a :exc:`ValueError`. This will be either 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 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 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 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`) .. todo: haven't decided if this is how I want the api look before formally publishing it in the documentation: .. _password-hash-backends: Multiple Backends ================= .. note:: 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. 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: .. attribute:: PasswordHash.backends 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. While the names of the backends are specific to the hash algorithm, the following standard names may be present: * ``"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. * ``"builtin"`` - backend using pure-python implementation built into Passlib. All hashes will have this as their last backend, as a fallback. .. method:: PasswordHash.get_backend() This method should return the name of the currently active backend that will be used by :meth:`!encrypt` and :meth:`!verify`. :raises passlib.exc.MissingBackendError: in the rare case that *no* backends can be loaded. .. method:: PasswordHash.has_backend(backend) This method can be used to test if a specific backend is available. Returns ``True`` or ``False``. .. method:: PasswordHash.set_backend(backend) 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"``. :raises passlib.exc.MissingBackendError: if the specified backend is not available. .. index:: rounds; choosing the right value .. _rounds-selection-guidelines: Choosing the right rounds value =============================== For hash algorithms with a variable time-cost, Passlib's :attr:`~PasswordHash.default_rounds` values attempt to be secure enough for the average [#avgsys]_ system. But the "right" value for a given hash is dependant on the server, its cpu, its expected load, and its users. Since larger values mean increased work for an attacker, *the right* ``rounds`` *value for a given hash & 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). Passlib's :attr:`!default_rounds` values are retuned periodically, starting with a rough estimate of what an "average" system is capable of, and then setting all :samp:`{hash}.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 much lower than Passlib's default. .. [#avgsys] For Passlib 1.6.3, all hashes were retuned to take ~300ms on a system with a 3.0 ghz 64 bit CPU. passlib-1.6.5/docs/_static/0000755000175000017500000000000012560246762016676 5ustar biscuitbiscuit00000000000000passlib-1.6.5/docs/_static/masthead.svg0000644000175000017500000003772312555044153021213 0ustar biscuitbiscuit00000000000000 image/svg+xml 0111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011011101000110111101101111001000000110110101100001011011100111100100100000011100110110010101100011011100100110010101110100011100110111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011011101000110111101101111001000000110110101100001011011100111100100100000011100110110010101100011011100100110010101110100011100110111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011 PassLib PassLib passlib-1.6.5/docs/_static/logo.svg0000644000175000017500000002522312214647122020352 0ustar biscuitbiscuit00000000000000 image/svg+xml passlib-1.6.5/docs/_static/masthead.png0000644000175000017500000001722312555044153021171 0ustar biscuitbiscuit00000000000000‰PNG  IHDR´4#}êsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<IDATxœíwœÅ•ø¿=iwg“´+i…Ô €@ä`ÎÙ€Èø€l“1©›`l~ÆÆ†3>|`cŒ Ø€Õˆ ØÐg96pd „PBHj…•VÚ¼;;3Ý}TÕNmïÌÎèö}>õ™î®ôªêÕ«Wェ1LËfF`[Øÿ6#0[[¢Ïu0-Ïu ù) Ó²CÏub@ ÓÄLËä7€9±äwCæé-kd¡‚±©Ä¢1HâEr” Ÿ¾“úºrf5¡AskŽgO¸˜®HÚP¦!ˆ8Œ·|Fá¬?ÀÀf´9ÛI}þ•Ù¾þýÁo;‰²U±Û¹ º¶2—(+‹å2¹ £­=iÆSÉNÿë'ͱ»6 % ºw4Ü8øë]PYmüÈχ3CˆÅ #cÄŒ?tt¿ù£e=¬#/ß²ÞHx®ã#DÈÉ´AÇ^Ù\MÊ­Óu#ðÏ1Ïu ä·>¿’¸‘sngŸ„aܽ×þéÊí&•F,Fks?ã'Õû̘’®LÇPUÍÍ£ÇJÅâ 2]9>z{AW6›=eñ²ìÌÆ+×®[±Ê7§íšˆ'Sû=ûkŽuõÙLjõªïj’Ú¤S8š<m‹¾amÔà«¥;.¿@’wL"9âË÷qëÔS—˜;¤ãÙlŒßnëìÉkBÂÚ±ÛUUï¸ûÄòÎö<￾$œ¶ÇTcìÄñ´4µ³ø½E¯xvn&~é^Î?yòî¸×þioáÿã…þäð ùФåo”÷MÝò›/CJâ…ŒÏÊ´¡ŒOPàà†|GþIÀs퀯˜–ýÔ–(3!9a`Z}6uYí¹GþÉä½ÇÔÖ¥âqÖ¬ÈøÝ]ùGO{ʾè±êõk:Ý.S^SWËîûO§²ºˆ“L•„þÞ¯ÞÇÔlšÆdOâÌtõ¨ ˆ“JWÅÓåÉi¦uN·¬£C«7/} ŸŒ¯?+ÜóÚ7ýùÿ”èá¹ÎtÄ„îºäo7PTj¡Ë´ìe[ ¾j`&ð¼iÙÝ%ÒLöaÀB0ˇ-Cв¢>ê3]_Ü/þ4£9ï@œL&Èòç:±Ó±ÛŸ8íþ5!±ˆS3ºÖÅÇ©¬©eê.Ó+–-øè£d7Fý„‰ùñSw6 NGËÚîl.÷†¦Ôëi›¾ëcߤ,bPjQòW…. X <×J6Ó–•ÅŸ±§1ïÕûùè…Y\„˜é µù½B†œü¾ ÒŽODohmêÞŽ®ÒÒÖ!d¶í‰hA”8² ‚y_¹…Ëÿp BN¸gô‘÷-f\KÈ™\¬`Å5sÚ·<ÐuÚSüü±/;K?|{õCµu•aݸÚÃHP]WCyZµ5¢¢Ä ün ]öÛ>íç7àçšèêðǯù„ë_{ÃÛCN±Ÿ´õÍaÑY­žëD71]¦e·iƒ*ãÏuöN—ï£Kà~J^߯auä}Õ–,\öáUžë\cZvnÐ ý z‹Á&{Ûþ;Û ²áÄæu—|²hí#Ë>òž_ôþ²¶bÄjïaèøm~;ßF¦³•žnŸT9LšNUºšÃ« î¤úÖÈ{Ñ]u¾EaRì6Ôö~Æ!yo)šj3aˆÄ м5ê‡Â¦0nZvÆs Ä’žÒ¦e·{®“BÈ¥ˆe?´µ_tÉ¿ÿO·ýŒý÷îZ]”˜µø­‚ óm4®lʯ÷2Ýt$ÔNšNz¬IÙòœzó çWg]ÍBÓ²[=שApén MTó\§Â´ìnµ‰Eˆ*å2_ˆĹÀAZ¾#=×ù›|Ö¤¶5¿|ä}( `kÂV[c¹9”„ ”úhE4ˆe<-Óò9'ãjMËnþÖψ…†ÑÜÝÙC?bî5Ú òmøù6º;›Y·2Ó½Üc/žÃî­í<¶n]F ªG“˜ÒÀ¡²>“)… ÐiÏG‘|ã)ÈÝÒûþæ³Ñ ï6ëßkÒ¦¯yYCK§vz–¶×lîç÷:Û:è/7ǃ,߆/Ål¦ƒÖŸq «XÓF^ü0„DÉXœ}"¸ „£‚h|ôy}$½¾¹¨ÜÏ:D9â¶ÚNH“¶iÙ9Ïu=tA¨åDv ê½ bc¥Œu@KO6ÿR[Kûac&ÄÊBbRe'”¾ßF{óZ2¨ªí&Už#™`üß˵«×³hÒx®¯¨¢" !—!Û•a±ÄAqfeÚVbD´†ÄMÅe%Þª= 4:¼)T\_q Ïuú‰žëÔš–•߇žëTõÀšaÈœzþ°°®”E®|ªÚs„iÙQ1g8ùSÀÀF`CÄ‚= $d§æ"¤œêx]ƒ o,6TUÞBUuMJ—›ó9Ÿu+ç…+–täzr„abÂöá˜ÊQæN¤›VóŠJò5õTWŽ&æç¡³•l*kZö:Y¾n†oõ\'JyÐôÆëøv{®³£öÞ ¼©u¼ž/ð\gp%0a¨õ\§ XÜÌÊÀIŸ…ë–Î)ŒFk:Û‡LË.©êò\§ ¡z<˜FÁ»Aæÿ«iÙÿ6 [•C{®3a•Ü aþ>ÔsÓ´ì¶a–cg#ú©Z~îûÇ´lw°2’{%LËî‘Ü£Ñà Ó²;<×)Cpð.ù“›¯j »çÊWïåsñDù^cÍ)†5¼Åó3«–Î7²ùØ‹ #ÓÁçóÆ=×3xzŒIº¶žøø)T!„!är°n9ÝaÈÇ\ÀûàŒBLœj +q¬¢¿ ð\§\nlÓ@4•#&ƒðØWËó]ÄfR™×{ Kz-Âq¦™‚Y·±Øø5‚ãoAºø…Ì÷&ð€ìãƒEpÛU”ÐÝz®sð0A¼·M²-§UÀ@åÐ[„ =×¹8Ñ_Q¨BƆ'ʲ*W…ðù°<×y8USÑöƒbY5®§ü‘Õ†,/ íBÎišŽ#D& "žJÞ1u×Ý«0`ÄéîèdÕÒù mÁtë² eZö'àÄ/þ!K_šÍ~k–ó_ëW1¹ªÈÏ÷íî ÞÞÅ“Ç4òÿ€Ä*QƒÐXTjõ*M†_~Ë =¥øRØÔÞDa _f˲~Öªìí¾» ü!Î7-»S¦ÛA\ÊGâ Ïu~\J ñ\çXà.Yî,Ó²/Ò¢oò\ç<‰GE‰ü» ôåµçÃôÁs_#¬¦EókåÐ[Jäø-b2^Jqn8¢ÙÕÝô]ÀBÄj¸°bs¯à(àÏu¾¨Æ$ ªaÊy_¹WöÎè"ò‹î¶š–í?ôcŽO•—Oªo¢Deóßë^¿!ü…uk´r}À8ë÷öâCÏ瘗ßá+¸aÙRn\´„ë0˜rì%\=é½ÆŽ n¬ &`±%þPíY7Ùû+çoCäwág=Ç—ÄÜ\¤wœt¸¹B««Œþ©Ã, «I?lZö}ËZ)‚ü9‰ÙQñÆ´ìWÿ ¿‚­Â¡MË~ôìŸ!V kú¥®,&š–}‰iÙ·™–}±iÙ3ýv$í>_¢ÐkòÖ–ë„ôãHh:ݤÔW«ÍS 0Èî:é©fü†©»íY¥DŽ–fÚ76v_s{p‚XÊ=שCꆕhsõm4ÿ ˜2®0¥8Ó)ëhB¬å2¾ŽþD} ðšç:o!Ìàj©û‚ØMùþÒçCóJ!YùNŸ L•içRœËDÍî÷¢‰<×™*ã”⊳¢M4 !»–ÿà+%âlÕM¡´ÌÎ’Ÿ†kʾ¥ØÆÏ´ìyžëü8O‹ºÀsŸ™–=?š'¡o#K§®âRÎ#ú,\ðý¯ÌþzUí¨ªšú†^­Æ'¾Ý†þ•m{Ø+Ì=1(]°>ƒu_ݬ­³E«»IzåEaW¢ÐüøiÙÖ#LËÖ7›yÏuŽÖÞ/a\‰G²X"„—Ÿ_¢¿¦e?¢Ð€·ôü³Šä_F_G­bði¨íŠÉPáƒR’ñ\`ZÛiQ——GÓoÖLýíÙN…7~¼XÚ„4w'zæÑò`´iÙëe|Ú´ì žëŒFˆ‰ÁɱóFW^YS'>‡1–Íû{g¦+w¹ñï4€"TzÛ›–½Hj(êMË^î¹Î8„,¾ ¡ÊZ‹à$LËþD¦­2-{­W|Ç®§ÿ¦ð-àe¹ôU#° ¨‘š‘PfZv§bò{Óx ¡EX,ËÜ,wRÓ²—x®ów„_2²­Ïy®óSàÆÁtÈò@ÅcIÁÝžë|¸Ê´ì¨‘h Ø +ÏEÞwUîúGåÓ™E,„ù7O_Ý´¾yÊÆ_!K߲˞ieÚ^¿z1¹lvéaó*…NÌ#½Þ¤O¬~° ±¡i‘õf%*­*£©JDˆ*Q“ƒÂ©uù-ÐÚ§°1z‚æ NÁüݤÌ}~±¸p%}uè)„zm¾ç:_Bþ§¯èg äü…žë\&'êPà3Í¡¤MBwj2(rÐCuHÁÑRÈ“àkY¯iÜ+ÜÉ‘‚3wÌ„I‰òÊ„]@Œ\†ÁÎÿ}_ì§¿~,øå£¦IæÉÎä³òYN#¬ˆkõªƒ® 埑¥ p×A7[÷¹·Ã+ÜãÑçd»×÷Ƨ@Ïçi×È<‡g"DšÑEê/ ¦e¿â¹Î)M†¾ªLžò\ç5àrÓ²‹ž1-{…ç:G"dì±ZÔhàvàrÏu®4-{Î ¨l ÄþK×U÷£…‚ãe¤O«âX…ÓÖq„,CÌÖ×îgBºœ¯MÚiò‚]Õue±Ý>?©|LCâìËÏàõ—fsé÷SàÄ) \91Zäî6«áÓ˜` ù¬N d\TöŒ¶##ñV‡B Y®~‡òKQí ªÇ@ýªgÏuFy®sBkòp‘ÄgX²µ´níPFað–ç:× .Âóh‘è?x®óÀ Üú3Ï¡%DñnŠ&ؤ«Àž8処îpêäé{'!Aàwàç6àç7Ï5áç6’Í4ztw¶‘‹…\›Ìp×Is†æ»à ~‡Æ­‚k€›†Ü;Б+Ïu.Cb*»÷G‡I_DpJýÈÐɦe?=X}²M'? ¯ÅRÁÕ¦e߆tßx7w{8ˆe5úßn >ÎÊ7D9B)NžFˆˆ3IMÃv²,Åy‰³r¾RZ ]­§h(qÐý2LÄ uÐw Ï‰Ô=†â›¬:„'Þ”ö:[¬=ç3µZ¤dþÇåwý ‚ú^0—þm ¬Ž:L¢0ÁÕ„ŽjBô6OCx'‚ð‹Q}©ð0þñ:ŒAh°¢¸€XiuPã«8 /A§€34¼®@N/rR?!Å À´lý0¥nïzôdçØTYrç±v0Â0 È·øíLÜ¡ªªº¶«jíòž{_{UAžËOÿ½ý0óѯ:„yÚWno7šRi‰p4¥™P î À¥•v%‹p¥¡ÐM  ŽRå)p\(8©g3+Ϻè·UäÚZÄÆéfÄnz,p2‚+êp<µŠ¾‹—~ƒ¸™(º2°H*ø½lƒZ5ÕmB³#(Œ‡NHzþj-%JéA—ÕAl¬Tœ~I~ògàI„ûêR‰W1ñ$JÐõ\1nåЧP¡‹ôl >6?þHßFá\h”Y¸‡Y%Jš–õ\ÇØi'R\Ÿœ7mï¦n˜‚ŸoÆÏ5‰ßˆŸo!¡½Ö¯¢Ãϳ`}+Wžxo!Ü"ˆV--UªÂ2­!j©R²—OË‚p-œ°.*Ø€p Ÿ‡V\Y)¢íÛè냬1BEžApÂhi\„\9‚Á€pUò§ªï ù½á29“þN=‹„ôÂwA÷sÙQ柊0L†XŠu©F¸\þ¾\:@ܘô¢ïtN¨ü¾ë›ÏqZ¾Ÿ#üÄ£½—ì}åü!pgäïÙÀO"m}á œÂòÀ¿ ÆQ¿]Œè{ÇR™’USò¹LªôÊ_p8ëÝßjϯ;3Ì®91ì^ñ…ðã—Æe–¿Zl˜ ›> \7—°ñ=Â5ï.|žàåûð¿z(ãe™µ6 5Ež«ešÄ×Ëo ˆ™?q…×;_Æ_‡ ¬-Ô#8l £K„I²C‹•ß‚ â±2<‰oBL†z1Aøøªø6„ÚïI„wžúþ7„¯^ *ÿ Zº9è#®ÔR&ûy5Þ 75 Jɤ›ŽCˆ„5r¼Î—õ”“…ˆUnÎêy0BªêJATT‰–¯×£ÿê–xަ/–·T¾Reè¸ê›À ò­.Ѿ*ö<|‡ÓÑçb¸ë!Â-…??Æ©2kãÞîS[^#Ÿß€ŸÛÀÇó>éloÉv®ÛÈ÷ϽŽgªÁ¸óÎ[Ïõ‰8Õ]ÝÌ9üB”«c1Ä /÷Ô¹)ôÕ#¶hyzƒ£ƒ4ÐéåFë*&ûéí QDÐz‚ò7O´¯Jí%Š•Uj‚ÔGƒá£ã5Øï@cÜÛ–’öÏuŒú8bå¹lÉ”Ao#ðÛ˜4-¬ìh¥2µ‚_þñv®Y²’ïümÛO~Õ9œðÇ7ùý—9CCD•Rb‚ú®7D-+ú="цF¨Çë0¡êuãÌDÒFÛ5l N;”²¢ñÅ&¢ú-5 £LÈÐÒGûm¸.š¾W.FȥƺOˆ×L/~Ø¡má»±®ùx×½¯cƒw`ë†&£¢"ŸŒmA‰ŒCª¬‚ºŠ8Çs<_ª­bîÅ7ðò{ó{u̺NUo’‚¸Ðž¡pª×•“¾Äékég÷#兩t1ŽõzPeæ‹_ûè9Šß@!Oÿ¶ 5ïPË-†[Éå[ Êr\¬¶ž…h}¥ðøoݦã™Nåõ—rA]­qMem˜gR‘HÒë¯Ð¼Ž°¹‘¦/žÇžôåljâˆQ*׿)ÿë¬Öq1í7Ô~‹A1.9T®WŒ[‡ë Æ™?M7 ï­Ñ®¡Š¥ž•·sàí…dNø&÷w`øðeçrig+߬M¢~;ÊŒ¸H”馧­“‡ŠTÝt¨çèÌ‹ŠQ®µd•Âw( §Û”%}SëÝÚ°)òöPa =¦¤jƒÆéæï!4’“Îyƒö9oð“óNâž³¿ÄwÚš¹ v ñŠ*RÝído}„Û(Nxº³“zÖ—0µš;`Ÿ‰1ƒÐPÝhc´ü.|Úxo  wlÔrh§T¢žY1À¸æbÆyצœÙÒÊUdzèŸÂD—†Rr«ÛìÿŒÀ§ƒ:økD]JÉYé‡Ý~—jëÝé"Eïó1ÀæÂN¬ègñ´ßbj­¡(΋i¶¹?íÿÖ¬"rì¦X´úî!À–„M:S¨ÃpNsŒïlmø‹È@Ø—V›IEND®B`‚passlib-1.6.5/docs/_static/logo.png0000644000175000017500000000135312214647122020335 0ustar biscuitbiscuit00000000000000‰PNG  IHDRóÿasBIT|dˆ pHYs|4k¡tEXtSoftwarewww.inkscape.org›î<hIDAT8…“_HSaÆŸ³3wøÞ÷÷>ßûòQ„”ŠÐkê¸®š¹£¦i½˜,ö5unûƒ}”™Öpü©n)´›Gˆ|b}ÕíùÍ$‰±Ç ËÒ+³ÕÜC¡ŒK­‹\íÑNP•¬@©éªLšNn&䨡ÆÄÛ\õ7ä¼j{q6|ÏÈÛ.}´ô}@åGÆZýÃF¯µjœ^«eʈ„»¥RsòÉFx:rC“eﺂâŠÃY³ÝãˆÌ~ŸË§ƒŠ`MÐiÊ**ÊA¨rì Ȧ'a©KŸiä0¤ø„ÀSJgsÚ}:ƒÕ n¤7sÒBŠa’†”_ØH`¢¶ׯû© %ŒôSÕG\Î÷v·})ük,<há½ßñHܧÑÂUeBÇ–ˆÕ"ÁØ‚ouæ:ëÑ›»ùtt.øÿÙÕv™¬À,n:·1¢¢ÑBòxÞz•ˆ „ì)ècë"÷XA¸B¤Å³Å¯ƒìãý÷¥´wù*‘ÏM“òj™ÿh‰Žr±ø ÓïðL  €é—5ÇŒ§Ÿoh8™“¦Ï éµ¥Ìu10)BVÚ=ñ†ãkëÝ~¯wÏO 3šL¥‰ÇE!6¾‚sm½dE  ~â‘·Vjäfš‰($yCÀÝ\õÁÖnâW*°o¸Eff 06¼]ýó§¨Ûò4-]äÁ¡ÿÆNZou¹éhIEND®B`‚passlib-1.6.5/docs/_static/logo-64.png0000644000175000017500000001026712214647122020570 0ustar biscuitbiscuit00000000000000‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYsttÞfxtEXtSoftwarewww.inkscape.org›î<4IDATxœÍ›yp]wuÇ?ç.ïiµöÕ²µx‰—@Šx‘d'„†C-ÊbË ¶dÚ™N”ÉÀôΆ¥P2$¶¤ eÚ[’8ILbÇv[ò"Ù–dmo»÷þNÿxzÒÓæ8Æ’83wÞ}O÷þ~çûý{¶ß•¨* %m»¥<KÂ Ž­¿WB™o:›e+Â×w©’ ‚ŠE—:Ux¨a»¾0_úÌû‘BãòÀ޼B;(,q¬l›p¦C4¢ŒôõÆýÑ!ß‹oÃWß»]G¯4f[›8n7•ç28»m›×¢×¼ÐÖ&N¸›mGV-[v‹JC€•<$ù©cß{ÏD8ulÈ7žOÝpÛNíIkïnYáX<(–l4V¶XåE5Ú.?Úüi=qµºÍ Íò¾kS¶™í2<|êð<åðó^<î<›ÁÖmÛ4hÛ-®p¿÷gådPXVàfdgÎÈ I028ÊåK—±H\}0çßÖß«Þ[é6ç´ï’:Ëæhõò°»¸&ƒÔª¾pá\œs]Q/ ¬œ¼Ö¬(pró3A,"#/ïï0úÍúSÜßY+ÏX¶ÔW¯¬tË«JQk2i`¡*œ?}Ž®co¨O¸ºå¶Oè¥+égÍ)zÀ²¸p*«3Ä&^hï÷OŽÇ£~«Qýû‘ˉ8Ø£ƒýqÀ"+'“ºÕå6ðO5|Ør㆕nÙÒ²iࣣq|/@Ħ²¦šwm­·ÝphYÈ“Ÿò \㜠ÂÊPØòIJÇÍþôñQxzÎò©¬ß¡Ÿmܡ߯ßa¶üòä‘‹ °›âòøÒÒ•VÖ¢ì1•mL œ?ÕËKû~Ÿxiß!>½Ÿ#‡ûñ˜G83›Ö­swwÖòÕ%@”êp–m§Wl†ž1ºkógµ?ýZ…–è¨2¶ã—ˆˆ–.)!E ØœxåDpúõSÑX4úÊfUîìë?ùÚó‡<(9y…T¯Zc¡üKÛn)ŸM?gNÑ'åÕÈp`” ‚íX"”N½P +-×ò-ÛqRæ]Q]æŒEÕq'"ÇÈ`„¾Þ>åC ;õ©Ôýû[äÅX4úÆÅs=”.©¡tI§ÁQsÐ:“rsÿXüÆ÷Œ1¤¬ ´27$ÂçÚwË]m{$§³Y¾(–²åC¡ «°¢jÜá&¢UÕ㳩7çÐØ¤?~pìðÅ`h Aj5äRµ¬œŠê’qðSÁN³±q¸aÝz7;/¿Î(ωG¿%ÖwJ*«k×lºÍµíðøõ‘áËžQ}}6ÝæÃ PÅ—.G©{õ`÷{Ë—R½¢\,;-‰™|Ú!“-!œ™Í;65:£ÃÃÄ#1r Kl'”1éšÞÓDG†B(¿›M¯y¯;šåSˆ|϶$;¯0×Ê+^d—V•bÙ³™<3£W /1Ì+ûžô¼DüW ;ô#³é3ï@²2T‡«pp×’åUNÕ²%\øH½9ªñh?Ç_zÞ ‚®¬ß®fÓeAH—ÎëÂ’KK×M!`Úª^xgàâiN¾ò{/ðƒ.D?\¿]ÿp¥ùçÍÌ.Zà†ÓÊãk4yD¼t†c‡‚ ¢|'è}«Ùç% Ì&m»%_Ç ym;¦xþ髞ö75˜` “‘eQQ[An~¶ ò-Û¢§³E~¶o—”̦Â`A €ʘqÕ'V~"NocàG0Á–¥´*‡Ú5ùöê›ó­Êe!Ëvø°esl«Ü9‹ '®ŽÎ`úª' ˘nòà‡'Á0Ê¢‚u7â.*$O•ÿêxTn˜ªÃ‚ vš\Ñä'¬ Ýä'À'- †‰G†‰Ž ˜ªIwQ¾ gâŠÃ¯žl•ìtÐ Šµ'°lמæìfr„&Ž1ÔD1A5ŒI~ö qé\$ lËÁ/]Œ³¨(9[E-îé£Ôf<|9¥Å‚XÀž=bw¶ÐòÉåï|§=£É§?ÀMþò¥N&2üÖ(a kÕçÛ=§axTÁvaQ!¤¤hÞó€C‹ÏÇ@þò†õë­‚’r4=û›–Øøc+AMl’¤ÎO¼2’HÄøeýý«ô¹:›¥5”ÉÇ–¬$¤Àèô¼ Ʀr˧õ<Ìó#ðÄw%œ—/¿ä}«nÞ`å•Ìjòˆ ˆ %è(^"‚ï%(®ÈÀqcæ%Ã1ÊÿM›PÙ›ˆñI%iYÉŸ­€[€Ça Ø¿G2óòäD¬[×ܲÑÎ)(ž^N¾| ¸ÔÓc§îAz1Zì%܂ŵ"ªIGç†ðq6-“&µØ ã«R…ÀO9Ÿºd^hÛ#9á¨üÚrìknÙlgç0Õäã‡ö—z?Ôïý{ÙlËJ®lA¡ Ý|¾£YºG›µjñ7ÀGòK“רB"žÔ'ãý9÷m»%?dËS¶íüÙÚõNfn>SMÞÊ‘ž †û/ÄŒê[š´c¶ñ=,y± ΓYºKúz ¿—@ 6@8/¯7;oŒàR7&2Ä™úZ=/<ß"E¾ÈïlÇ]½vS£“™½høÀ÷9r°Í½ÜQÑ÷\ÍÆhûn¹K„ÿ.YŒä—câ°pÃÉkSà#ƒp±>аCŸH5g<÷#) Âò¬ ×­ÙØèfd%[aé…Œïùyþi/2tyXŒÞ¶y§¾Úñ;šå~à_só1Å‹±,' &L>u>2çñMÀc MÚ”>ΜðÜ¥ÊÙë†3ªÖnÚâ†2²§yy?áñÚ'½Xd¸ßxzkÃgfo[Í&í»å.±ø¡X”äã„3!œ•äØóÀ‹ÃP~|å‘D÷ݶMGÒǸîìm•ZÙÎÌ*[³q‹ë†³¦åñ^<Æ«žòâ‘Ñ &Э÷èÉkïÉVÉÎV¾4¡Tˆ`4báe5|¡a§˜éþëJ@{‹¬´Döffç®Ù°ÅuBÓj÷xt˜×<í%bѳ~ [·Þ£Ý×kþÎV)5ÊQò°x×>£ÃWºçºÐÙ*7 Ò–™»(oõ-['fj.½Äkžõ‰ø›žÑ[§îý/„\—<`ß.Yg;òLv^Aöª›ÛÒáQƒ1#¼ñòó‰D"Þç¹ÚðVÛÖó%t1Ô±[6Y¶ìÍÉ/ÎY}ËÖiàU½ñr5¿4?„jEÈã¾?Zóë$Ô#°¯UnµUžÈ/. ­X_o[Vªµ*_£˜ñB&yÞßÓK×± *JóÙ,þúZßí¹^rÍìo•;Uåñ‚Ò gźM–Hx5Iàcµz²vŸ¨Þ†ú†é: Dy’L>²y›F¯+ª·!×D@G³Ü-"?/ª¨²—Ý´ÁIÕíI“× ÊÈàb‘ËäeM² "#2jè>†¯Êï%Áû¦¾+0_ò¶ èh–!ž¥Å£Ðußøœòn¿ž!ñjåmÐÑ"M¢è>Y€™C¼Óä- èh‘/U¯¾‰ÅËßA²5­cÀ'·¥ J]J«²øag³<£ñ3ú²/ÜâÅ9Ûu ?K®¾*x18{ÏV—Æù‹WôíÍò j×®§¬zIG—êÅ¥7('{ù¾ž(=ݪß«ßÁ?¤ŠÓÎV)Uå)ËbMeçßÀ3†ÔçηÊÛçBf%`«<¤Ê—êÞ±AJª–‘l[EÐ :ÞŒœ9Î'½üМ?‰QøyAÛ×lÓ$Ûc¡(#lÁ˜€öŒ8\¯FæxJf @¤£…ò¹å7m¶Š*kP5›AòóÜÉ..t_ÒŠš\)(5Ñd ;­1:çÞÄWÃÞD&JÕâ¯í‘Ð@”ï„ùâûÿNãóŒ{m:{öˆ]ãQQùôŠu VaÙÒ4“Ž[@÷ñ.úΨQ~!ðÑ¢ (©LŽ15ÌÅ¢pö¾þ¼wË=zqAÎ"“œàâ¨Õ*XŸºáæ[­Â²ê±<~dÒ\×ëojß¹c”MºM”{úÎcΟ¨™æBÉ0g;¬µlž{îGR¶0Pg–ÉQ@¸-3'œü¢Y·¡z‡Tá·MÉ7/ëwên»ûðΜ$0†ñ&eŠ'9¸(µÃÒÀ9«L"@sgtx°ïÕÎßx±Hߤ—:¯Z²DxOG ?}m„ê›ôExwdˆÑ3'ðÓ€^¼ˆ¢ÜW¿SÎ?ÌÙešÜ»K–8¶<í¸NͲ›jÜp8˜ìL‚Ñádúj”v<îN…¯­²:Pž …(*¯ÃµèïI‚WøBc“>¼ (¯ 3†ÁöǤÀä ,Y_»*ÏÉÈöQA5_ÙxÎÇóŽˆpGêM¬ç~,UÏ3¶CMæ"œ‘~¡©~‡þç¼£» ™5û•Ÿ‰Å×açä%O÷ò‰8œ=çûœ3>·§º»û‘BuyB•õ¯oÒ_Ìž·-W®«£–ï÷–-Eò‹¦„9À÷àÜI|/Æ †?¯ß©/ArÜÏ&sã§th>€\«\U9ÜÞ,_øzQ9VŒ9·4Oo ô¼I%®Âv謯¦þ©ÉUUƒMú U>Û׃éíB1à!Ùú/]Š-6™_›C}¯»\u? q§>*p÷P‰s§L0êL½]ÆîŸC}¯»¼í–X{«l´”_‡2É.«Áè=Eˆ2j”Û›ôМh:GrMMÑöYiÁÓ¶K™e#^Œ!1¼­ÝÝ?¹æ¶xÛn)w-ž(RŸw_Ëî,øÛâ -ÿºY¶CAT¤IEND®B`‚passlib-1.6.5/docs/_static/logo-128.png0000644000175000017500000002026712214647122020652 0ustar biscuitbiscuit00000000000000‰PNG  IHDR€€Ã>aËsBIT|dˆ pHYs$é$éP$çøtEXtSoftwarewww.inkscape.org›î< IDATxœíyœdWuß¿·¶Þ÷éîéée¦gß$fÕèI`$6Ì¢OP\‹ƒäÄŽ„‰?"JbÄ&, ÏÈ@6(`–FЋ$„4Úf_º§§×™Þ×Úoþ¸¯ªÞ«zU]ÕÛôT÷ïóy3¯ª_Ýíœ{Þ½çžEH)YÅÊ…çJ7`©aèÂ\ ljÍK]Àà¬æ——®\ —b%HC>à_ï~ ¨™å'¯?×üò7‹Ûº+‹‚fC¥ÀÇ?šçXÌ/€5¿|zÁ¶ŒP° `èb7ð°-ñ¥€²r•5*ªÜx}.¼^RB0 %#F†BD#iãòKà#š_ž]º^,> ’ ]Ü < ”¸ÝÐØâ¥¹Í‡¯Ø…zå[.aÿ,¥`l8LÏ…iFƒÖ¢Çj~ùýy¶¯hªÍ+ŠZƒti~ÌöÛ…FÁ1€¡ ?ðÍøçÚz7›wãõ:ÞF|lßKóÿ‰Ñ0§Ç™ Y«ù¢æ—ÿ!ÏvíÞ ¼ 8ø“@/ð$ð8ð/š_NçSO¾((0t¡O>!`ýfÍ|8Þaæ[ o»¤ ëÜÝçÇ­Õý•æ—ŸÊ¡M·ŸnC—¦¿3ë˜ÃïgEÁ0€¡‹5Àq `ãvM­ˆï@øŒÄ·\#ƒN¿v™h$¯ö>Í/¿¡=•À#ÀïY¿w¹]TT•R^U†ÛëÁíq#fBg‚LŽMœI{ Ì_þ‹æ—“s$|¸`m‹—M;Š™‹ÈŸí püh?2&A‰ì·i~y$¥-×¢ [âßUÕUÐÜÞ@UmB¸˜Ó3 _fàâ¡€NÐüòµ25…À†.Öç€R_‘`ßMå¸\óù¶‹´ßöMrúµ„D>\_¼ºØ ¼”—øØríz*ªËU}Yo« A,&¹Ô=@Ϲ.BÁÄd¸GóËGbì\ QÈ2Àb®ø[Ú‹p¹R|"#ñ»A< âO@Ü â³ ~B&Ÿs©K¸÷kšªhl©Š×½øOÐ;|“øµ U\{㶉o©Ë¼\.kÛZØsó Ô55Æë+¾aèâß.ÄÀŠxÐÜÁ¡7W"\)ÄOŸ]a‰xă¢©7ZžìÛp#ˆ/ƒ¸>“䈄c}¶ƒp(jV®¾| fM;ömJ„:Ï(»pê4•µu”UV%Êñ—Ò¼e+]'ƒÚ|ørÞ½Ë÷ˉӹé©öYŸF€¶ü‹×:Ïfu•U”™’êÖÖ D†3cCcŒ\Ž| ¸=…ø h~ùÀÈXŒžóç±J‰ ¡eƒ¹åà£ù÷­0à$˜žˆ¤Ìú4‚Ì¿x¡e›ÑnŸ‡-×¶Ó´a-ë·µ‘*ò­×ø°í,áãš_¦mA­0•='&FFˆ>þJóx‹¨i\ü:C[óíÝUϦîÀèpˆX²¬Uömü`®eËþÝ׃¸3ãJÞ\èÕ6Ö²a{n'å;N'ÔøAàù›ñ"@(  ¦µ¡¦¡Éúìu¹ö-Ž«žL| ‘ È6cA<,û7o˜­@ÙM)¸¾.o*!å,ºüL—åÝ?¦ù¥óò?+RʂҊjë×ä[b¡0€ŽÒŒÑß=MF1¬TÂU ž‘ý[oÏT˜ì¿æ-¨ýÿ®L³>óº 3”V$v †.ZsìÛa_Q1Þ¢Òd=æN£¤¼ÊúlÞ¯€BØ ùe·¡‹'€Û&FC\¾É¢Œ?ùy=ˆÈþOƒ8œ1 b¿ùÎG6‘?׫¼ªŠKÝ}ñfß |2[¿ ]ü° ¢¦Î©ê¿y P$(eH óÔ(‘°$ñ­×- þ;¸×àz\ïX(‘ŸÊ€ukñø¼ñöÞgèâŽL1tñ6à!—ËEóæŽý0¥ãȺ¨tBAHÍ/Oºxøt8åÄËì:°—{~„ËŸð¤g–áñùØ|ÍnN¾t4þÐw ]|ø"ðÊ6p?p'pw¼o­[wSZYíXW8h³˜ÈwÜ Iü7Ô@21äÔ+Héô¾^,â;Ô•RFMC-›7[Û|;ÊâxŽ`!~cÛFš6nÍXרðekY¿ÎwÀ Š4¿œÞtŒ NóƯ{˜™ “•H‹$òñÓËoݲ•݇o45{6$ÞèE%¥ì8x3í»÷f­gìr¯õ÷yû.\õÇÁN0t±¥Oo.AËÆ5´´¯A¤‹,¢ÈŸõ’024Èôø8©)¢Ñ(e•Õ”WÕRQ»árg©Kœç•_þ‹Óürw¾cU `è¢øð–øw¥åElÚÕLEµýˆu~"?•øäü¬£9Zm2 2L牣 \8ïÞÇ4¿üz¾ãT° `èB täŸò¶¼ª”êº ªê*¨¨®@¸ãoÂyŠü<¤ÆÜ‰FÊ0ãCœúÍsq[„`Û\l šâ0tÑ |xwêß\nÕå4¶5P×w^$‘ŸiÖg,ÃZ—DÆÂH!05Æñž%NÐûÍ/¿;—±Y ‡¡‹÷~”‡°Í’Ãív³ÿ–½¸Ü©ïÝ|g>Ÿ¯È—2L`jœS/½@pf&Þô/h~yß\ÇdE1@†.<À”·ÎÇP~z¸u?¯Åo‰|d„ÉÑ!Ný ‘P¦åŸ÷h~™ðTÉ+’¬0tñ8ðáÜðöÃ,G‘/e˜‘þnÎ{ƒX4oúO€÷k~™0-š F84x}ó™ù8>»"?83É…¯3zÙ¦ðyøÃ¹(Ú±Ê ÈâDz…Dþèå>ξzÔ:ë%ð€æ—ÿu¡:¿Êq(ò’TŒ^)‘Jˆ|d˜þÎóVâÇþC›€B¹ç}dEA©‚ó…¡‹ ”A¥E¤êòs?óÏ8ë32©z–1“øö«¾¹·'ÅæUÅ7ò£ŒG/ºxØÐEí\Ç`E3æì‡W@„KÐnzn/#'‘TD…± ùˆª5ì¾q7[÷n¢uëZꛫ(*± írà€ã†.lnè¹b¥¿ à+²0Àùñ{)ÃS÷‚0%enŠKK@zX»¾„™©cƒ3Œ†e¼ºøGàù“X•&¼¾"®´È·ÎþX\*È’±¤„(*ŽRß,Ø´ÛEÝZ’¼¥‚Q¼fèb{®°Ê&¼EEóùÎgÿùŠ|;3¤þ-!„ˆRß¶Cqi¢O À“†.Öç2« `Â[TÄüE~6É!íežáÎÒÁÎS^âRýv°·Oº¨›mVÀ„×WÌrùöߪ «7~ßԥɎͨSЬXe¡ìõ–›È„ƒLŒÎ0Ø;ÍP_ɱ(áP’àû=Àºvð%úw·¡‹7g€{`è vy}>ößz ó_å[Wûv]~¦U¾”éÏ„ƒAzÏO0>â¬é­¨†úVðzã5‘à LO@O2žéIT #ÇÂV¤0tQ„Šâµ  ´¢‚…ùñY?w‘?ri‚3¯Œf$>ÀÄ(tƒÑËéÄGBi9T$UCÛwe*kÅé ]”?Þjñ×¾sÖwæYó÷–™¯1%@ØülŸí2–.&GtŸµî½| xÉü| ÊÂé:ƒ‹àñYÞû2ù*¨©‡‰„#2EiÓ°¢^f<¡oð³ëà!ŠË’^DsùdÔBÌH^"_Ê0‘pˆs¯M%èñiàó©¢ÛÌwp/ð×n¬ßno’øqFè9 ÅOQ ÙIA´b$€¡‹*Ôúa€¢’v:DQIÒ@tî'x©3ÜüKa˽ŒÙ3Ô;c%þW5¿üK§~˜Æc袸/Ñ!¨]«þnÏåÕ p£âÿ(µ¼±0÷ÃOc¿¸¬ŒÝ7ÜHQI9ó[åcYÁ;\y(v¦&§~Ó@.&^Žé ˜BÍú”­aQ‰íyG“ñ‚gCÀ3À^€Òòrv:Œ¯¸„ÙE~’øBÏ$f̺ “AbÑ ¡™)¦ÆÇLM뻯 L'¨÷z.ÂÍcàW‚3)âßüà-¶ýd—S9ý 0­`&(«¬dçCx|Jë7‘D&& ¦g¦ ‡„ƒAÂÁáPÈýK¨o® ±µÄa EJ°XõÙˆÌÛ.U$º¢¾/ˆ.6 ˆ¿ ¼ºšûáñúòZåGÂa:¿ÌÄðeÂÁ±X4½²Y %\î™ ¶Ñ…ÛI®ˆ%(XT5ï÷åØ?/f@_±³f0®*6áh8R¯C[P—*kkÙyà†YˆŸ.òÃÁÇŸšÁžN‚3S¹´Ú@éþ'ðmPĽ<í¨Ë(IF›«5tñ‰ºù ¦1KQò ÈÆvc"fp@ÁI3Z÷`-@Õš5lß{—Û“—Èœxáif¦¹åyÜôYþOÜk~™æžmZý.P6r9BMƒ¢Pꌭª3;êû‡ ]œÈ”¨ÊÐÅ`—p{ jMJyæ?!ûJâœSY¥0t±³w @MC#[¯ßïàd™]±œ™æø OœNuzx»æ—Csl×ÿîذ#yt›ªÅ½—ºm?}%AN "¡íEéÞ q½íÈÆ#0–4&>¬ùeZ`ª‚‘†.¢/VÔ­mbËž½ 2ßyæ¦&9þÂBÄôyømÍ/GçѼG0 ÿ‚RÜà"M…[]O²Ùæ•! ¦13ñe ¦Æ'Çü‡±0tq3ð&ñë›[زgŸ3ñ³èòg&Ç9öüSVâÿ•d>ÄGóË_™e˜†¾ ¤?¾hl…um'zi(*…u›¡ªÞ,òõ‹ŸNA$÷;™ƒ®z `èâ­(=w)@ckí»®§ˆD>¦ÆG9ñ막„!WžDEò\¨¬]@Í–ñaEÄš8ãO˜ŒPZ må03 Á„ÍÈwE%êwVæpz÷Ç¢êub"„Šœâˆ«z `èâ]¨Õv1@ÓúvÖ&|D>&G‡8ùâω„SæÇÀû:‡Ÿ¡‹ýÀ¯€b! y”T`—æÍl÷N§€1 —/$TÀ_ÖüòÞLmºj_¦ô0‰ß¼q³3ñg9¾¾Ì‰_±ÿ1ཋ‘ÀÑÌCü1P3·çœš©‰Ý )ÊDÎrŸ‰øÃ=6â¿JY—W¥0#i똑Â[¶l£eóv2ÏzÒÿ†`l°ŸS/ýÂê}ó(p—æ—ùk{òkÿ_`!Ly¬Yîx*Ië¬ÇYÉc>–x0†Á‹J-lb8¨ùeG¶¶\u `èâ#¨dŠ.€õÛwÑÔ¾™|Ô¹ èâÌQƒX,¡ƒý:ÊárήÖyöã.Tž€RPM®¨…šu´ ä$ò£Q˜Rgÿ=Uð¯4¿|a¶v\U `èâàK˜Óº}ç54®ßH¾Äêëàì+Ï#“´þ2ðÇšiÃÐÅ.T–±‰/…Ò”V@q9x}æzÖ„D­îƒS03Óã¶sP_wäêrÕ0€¡‹ûÏ wï¡¡e=³ÛýåýE,ýþ¼æ—YC¶.&Lþ]¨Ôsëžq¹ÀåQ„ŽEí¯ †PYË>—Ï+ìª`CŸB°éÚ½¬Y×J>³e ë4Ç^¶ý æ—,]O2Ãd„;÷·`I…3 ΢^%_›K°ˆeφ.þ ø3ár±eÏ~jבñ¥ŒÐßq‚ 'mcþ£æ—Ÿ]®ä Ce¨,dס¤ÂzT”³1óº <ü\óË ó©kÙ2€âí¯1Ó¦¸\.¶î=Du}Ü.‘¯L³{ΠûÌ1kñÿNóË/-QW–5–¥&Ð4|ü[Ì=³ËífÛ¾¨ªk ÷Y¯Â¬\<ó:½çNÇ‹–¨T-·´=Z¾Xv `è |óÄíñ°}ÿa*jÖ;ñ•±e×ÉWéïLDÒŒwk~ù­%íÐ2Dzbs!ômÔB×ËöåUfÇ<|î{η? Ü©ùå÷–°;W– ˜Þ:ßÇŒæéõ±ã ¦râäYKÊCý¶ll®ßËâ,ÀPi×DœøEÅì_Þ¡ùå©%„e„%eC¥q9´ƒJ†¸ý€†Û“=NošÈ…ˆÆÂ¸DÔQä§3Dòs4ãâeogbx—æ—ygÜ*,*³õT+*ëêÙ¾_ÃåNÒ ÙD~(8Í™£¯˜š¦¡µ‘¦ Ž"ßΊQ@&üðz:`2¹˜BÙþtIcaIÀ4|x Ó[§º~-[÷ÆåJͶ]䇓œ~éÓI»§ÚµÕ´l^¤ûÜ[0!Ýn¾ÿ"Œ &¾ ÖüòÑEŒe†EgC×ÿ‚é­SÛ¸Ž-×ßÙ['Ã*?0=É™£¯œIß½U֖кµ !"ék€¸¦ƒ9•{a¸?Q”îÓüò‹‹0ˋʆ.?%î­³®•Í×BØlö!›È—2ÂÌÔ8§_~p0a¹û,*@Âw1s–VxhÛV‚Û•|çCf#Jëýè \ºhkúç?[j±+ESºxjŸ¯¼uZ6°%ø‚Ù‚)NOŒpú¥W­Ä?‚Ú¾=hÀy€é‰Ç& ‡²SÌ ªê iƒ©xT¸ø¦¡r 4E*óõ‰{ë´mÊš5“bgz|”3¯³[ø1*ONÀR×Z”6q(#ÊÖ­à3½g²‰ÿøMü~fú:mÖµO°H>Ë . ]܆:Ø)hjßJûî}ä?7žâôÑ7¬Äÿ>жE€æ—ý¨¨_Ï„Cpá¤rŽÈJ|S"X¡¸ÖmRF˜&~Șâ½°  `èâ}¨T&EÍ›w°~Çòùq†˜#ILÅ*r–cfLÍ/Çwšu@×é¤wìlRÀêu39 IW"À‹s‘åcCwÿð´n»†Ö­N~z8ߦFÖ*«*¢zM…µOº¸=Sý¦˜~?ð0("öœƒña3=á~e½·¼{Òœ*ß§ùåñyͲƂ¬ ]|5ðÊ[gÇšÚ·‘FüD0ÅÜuù=ç.3Ô—PÛE?Òüòk³´çAÔ.€ú¨^£î39]^ê¶EÖœAyÖülî£ru`Þ `èâ^ào0§wûî}4¶¥¸j‘L1š—.àâ—ºm!nþ\óËŒ.Ïf»>ò"rÔ6Bm“ú[ªBèR7LŽ$¾šÞmî2 óbCŸ> „`㵨onÇQä§iér‹Ÿgˆá }¶_A¹seôå3tñT¬]@e­’qõC,—ºl‘4ÆPÑ@ž›ó \e˜3ºø ð(âoÞs˜º¦6œE~ê™}dÖã['†˜žó¶ü=àßh~rhb¼·¢ÜÈ+ʪ ±M‰ÿ Ê·ÎÄêdð%Ç‚ sbCŸÅŒRår¹ØrýÔ4Ƨ–“ÈOîïSOç²ÅÏuÒåOO@÷9Û^ýiT$´]–öîEé ŠË”ÒÇr"8€ ózÞƒq•#/0½u¾ÜÊacëÞ›¨®obv‘¶tzr¯ÜîXv†pÐå§¡û¬ò‰7ñ2Jt'×ðémߌ:“hOùSpëJµ È™Lo‡Q±çq»=l;ð&*k™]ä+bÆ ·£—®A„Kжµ–ÊZ¯£È‡ÌºüPzÎ@2¤çP¡ÜΓ¦Öð§(Ÿ;€Nñ3þ¦Ð‘˜Þ::ð!·ÇËöo¦¢¦žÜD~òûžs=\î¶•¿®½”šwöã[FˆDÔ^?˜ ã4€:'x%K_ªPA“šQ>‚3=»0+˜Þ:ßA%%Äãó±ãÀ[(‹{ë‹ÈW qñLC} +Œ0¦Ò ¾ÙÍš&ÉlÇ·©Z¼Xú:ÔÚÀÄ8jMðóü‡cå!+XrëÜÊacÇÁ[”ÃF"_Ê]§ºH,¹§QáSÛPá^ܠ¦5¶%ëÏ•¤T+z‹…Oå öØÇeÅ #˜Þ:c†%õ—°ãÐ-””U‘È—±Nö0z9±äž~GóËgÍzÞ:×/•kÝF{[¬*\²ÜöØ,|bÀ=š_~unC³2í,àÿb¿¨¤Œ]7¼Õ$>8fɰ%LHêø;w[‰?¼5N|Í/„ †0*#ÖÅÓf(’çø³ uë’©S̾ý¯#o¯X82€¹eº5þyÃÎ}•V’5K†CLŒŒ36”ˆZEíµÓ¬o5¿4€›Q[2¦'Õi^<Ôi¦ã[§ûêz•+Ç‚»ò•GÐüò,Ê€³¯>Çè`ã Ï–ÜØW,q¹f6nà3m›SÇ€Q‰ Î(I âx‚çt”+%Œ ÙÖøsš•l¯€ÛˆFœzñ— ötÌ*ò­ áñFißY†Ç›`‚w£2[×8U¨ùep*B7áb‚DìæÔYŸ²M„¡¤ŸO eåûíü†dea¶]€@öÜÿ®eË&ÛšÒVùNÛ¾øB0qáT”pRis ¥wïI¯5±ý>Ê"— Ön0ãê‚£ø½¤â㛈¿¯ùåwò•†\Aÿø"æÆ¿¡u-Í›š·}é;0Èá0tŸ±ÌfèB1ÁÉ uz€o¿JwßЦsâˆ7}d ÍãÍ/×H¬Pä£ þ ð÷˜G«5õU´nk@E¦(€°šv›Þ8 VöÝçlJ›!”[–cHS' T·Îž"e¸Ï¶õ ¿§ùå¬"'ä{t êhµ ¼º˜õÛkq¹¢nÚêì>õ=-¥ÒÜM$jÓ(Ó«Ÿd©÷O‡0%PU½Úî öª8¹–r~Wó˧rîÐ*ò?6tqêhµ  ¸ÌÆíe¸=Q›ÈŸM…{©K9d˜ˆ l#x™6‡`ª½EX×(Iò«¼:³Š9Ûl@ªmð Öoõà-Š`ù]…;ÜC}¶Gï×üò Yê}'J5]fùzu4kdìU¤c>Au(G@¥/kݒ̈™r­÷£ƒpÙž)ë!à“™Ü²ÌäPÿŒò5B)—ŽÎ©«˜·M`)J¨íÚºæJ=‹øOÝÆMŽ©ÃKSþøH¦DGf‰Û€Ÿj~Ù9ç¬bA¬‚Ý(C‘¨am«¹R‡œafB¥TKqËzÿ&mZ…Ì70Õ¿® êâ9íÿd×å‡Ðw^yö˜x¸m® W1;Ô9ÔÐÅÇQÙ7Ü ¤@C‹ú›#áIß&FBÐß¡ÔÀ&N F+Úrg±° ¾š_þ-*Îo”‚¦·Ãâk7‹.ÀヵUFl;P‘ÃW±Xpï`Í/ˆ²#åtÑsN‰u§<+#ÄÏÿ¥´9h‚yL¼Š…Ç¢ˆ0Ï÷o.‚rÕî9«Ä{šfÐrT¢¿ÿ¼-íé³À}‹ÑÎU,~ˆ˜f”Âh7¨thMíJ¼;‰ÿpP½ÿ-‹À#(õnÞ¹pV‘–"HT5ʶðM.·Jy^\fßp©ÓFü'P;«1}‹.ÞŒÇûvàAíóû;L«8ñg`À>óÿ åž½JüEÆRŠt¡ÜÈï‰WÛ¾¸dW}ðgÒ®baq%b ø‹D„m=ðð±¥JߺŠ+À†.ü¨ä Ö0l_î] ±ù–®d¾€ßFÖÏÒI(IDATÙý•i~yÿ,?YÅ"àJg ©ÚL“ðU\\ñœA«¸²øÿß礫&?æ¿IEND®B`‚passlib-1.6.5/docs/_static/logo.ico0000644000175000017500000000217612214647122020327 0ustar biscuitbiscuit00000000000000 h(  °Ñn®Ìt±Ñ†Èãõ¸Öܢŀÿ¹Øœ ÓìûÀÝרËO³ÒŮΈ ­Ì2Èä×@äøÿ ¶Õ½¼ÚÌÐéÿ³ÓÀ£ÂªÑ'1ËäÑMçúÿ'Ýóÿ·Õ·¬ÍG¼ÚÚ°Ñ›ªÑ!-ÇáÎXéûÿ½ÛÄ ÈâãÏèü³Ò  ªÆ(ÅáÊYêüÿ;ßôý ²Ó’ ®Å%ÁÞÇUéûÿ&ÂÝĪªª¬Ì_°Î’ªÍ3¥Ã"ÁÞÂQéûÿ,ÅßɯÌ#³Ó›Äàñ½Ú仨ä¤ÈÄ ¿Ü¾OçûÿBÙîð*ÍæßÒëñ¼ÚͩʚÉâû¥ËD¦¿3ÕîíNéüÿVêüÿOçúÿ)ÝóÿÍèþµÓ»’¶'ÎèáDéýÿLéýÿSêüÿ[êüÿ²ÑĪªµÕHÍèÕ0äüÿ9èýÿAèýÿ@á÷ö1ËæÞEÚðø °Ñ= Ãáº澯 ²Ó¯.åýÿ,Úò÷&Ìæã ªËu:×îý±Ó\Ààª×ñêÅâÍÐëæ¶Õ’$Òëê+Òìõ¾Û°¬Ð+½ÝŸ Áà­¯ÒP¢Å ¨É/ÿÿÇÿ‰ÿÿÂàðÿøûüaþÿÿþü üþpasslib-1.6.5/docs/contents.rst0000644000175000017500000000101012555044153017621 0ustar biscuitbiscuit00000000000000================= Table Of Contents ================= .. toctree:: :maxdepth: 4 Front Page install overview new_app_quickstart lib/passlib.hash lib/passlib.context-tutorial lib/passlib.context lib/passlib.apps lib/passlib.hosts lib/passlib.apache lib/passlib.ext.django lib/passlib.exc lib/passlib.registry lib/passlib.utils modular_crypt_format history copyright * :ref:`General Index ` * :ref:`Module List ` passlib-1.6.5/docs/modular_crypt_format.rst0000644000175000017500000002241112555044153022230 0ustar biscuitbiscuit00000000000000.. index:: modular crypt format .. _modular-crypt-format: .. rst-class:: html-toggle ==================== Modular Crypt Format ==================== .. centered:: *or*, a side note about a standard that isn't In short, the modular crypt format (MCF) is a standard for encoding password hash strings, which requires hashes have the format :samp:`${identifier}${content}`; where :samp:`{identifier}` is an short alphanumeric string uniquely identifying a particular scheme, and :samp:`{content}` is the contents of the scheme, using only the characters in the regexp range ``[a-zA-Z0-9./]``. However, there appears to be no central registry of identifiers, no specification document, and no actual rules; so the modular crypt format is more of an ad-hoc idea rather than a true standard. History ======= Historically, most unix systems supported only :class:`~passlib.hash.des_crypt`. Around the same time, many incompatible variations were also developed, but their hashes were not easily distinguishable from each other (see :ref:`archaic-unix-schemes`); making it impossible to use multiple hashes on one system, or progressively migrate to a newer scheme. This was solved with the advent of the MCF, which was introduced around the time that :class:`~passlib.hash.md5_crypt` was developed. This format allows hashes from multiple schemes to exist within the same database, by requiring that all hash strings begin with a unique prefix using the format :samp:`${identifier}$`. Requirements ============ 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 conventions followed by the modular crypt format hashes found in Passlib: 1. Hash strings should use only 7-bit ascii characters. No known OS or application generates hashes which violate this rule. 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 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 (c.f. the list of :ref:`known identifiers ` below). 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`). At this point, any new hash schemes should probably use a 6-8 character descriptive identifier, to avoid potential namespace clashes. 3. Hashes should only contain the ascii letters ``a``-``z`` and ``A``-``Z``, ascii numbers 0-9, and the characters ``./``; though additionally they may use the ``$`` character as an internal field separator. 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 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 :data:`passlib.utils.h64`). 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 to a "configuration string" containing just the identifying prefix, rounds, salt, etc. This configuration string then encodes all the information generated needed to generate a new hash in order to verify a password, without having to perform excessive parsing. Most modular crypt format hashes follow this convention, though some (like :class:`~passlib.hash.bcrypt`) omit the ``$`` separator between the configuration and the digest. 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 hashing should behave the same in either case (:class:`~passlib.hash.sun_md5_crypt` behaves particularly poorly regarding this last point). .. note:: All of the above is guesswork based on examination of existing hashes and OS implementations; and was written merely to clarify the issue of what the "modular crypt format" is. It is drawn from no authoritative sources. .. index:: modular crypt format; known identifiers .. _mcf-identifiers: Identifiers & Platform Support ============================== OS Defined Hashes ----------------- The following table lists of all the major MCF hashes supported by Passlib, and indicates which operating systems offer native support: .. table:: :column-alignment: llccccc :column-wrapping: nn ==================================== ==================== =========== =========== =========== =========== ======= Scheme Prefix Linux FreeBSD NetBSD OpenBSD Solaris ==================================== ==================== =========== =========== =========== =========== ======= :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.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 ==================================== ==================== =========== =========== =========== =========== ======= Additional Platforms -------------------- The modular crypt format is also supported to some degree by the following operating systems and platforms: .. rst-class:: plain ===================== ============================================================== **MacOS X** Darwin's native :func:`!crypt` provides limited functionality, supporting only :class:`~passlib.hash.des_crypt` and :class:`~passlib.hash.bsdi_crypt`. OS X uses a separate system for its own password hashes. **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): .. table:: :class: fullwidth :widths: 1 1 2 :column-wrapping: nn =========================================== =================== =========================== Scheme Prefix Primary Use (if known) =========================================== =================== =========================== :class:`~passlib.hash.apr_md5_crypt` ``$apr1$`` Apache htdigest files :class:`~passlib.hash.bcrypt_sha256` ``$bcrypt-sha256$`` Passlib-specific :class:`~passlib.hash.phpass` ``$P$``, ``$H$`` PHPass-based applications :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 .. [#cta] :class:`!cta_pbkdf2_sha1` and :class:`!dlitz_pbkdf2_sha1` both use 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. passlib-1.6.5/docs/overview.rst0000644000175000017500000000721512214647123017645 0ustar biscuitbiscuit00000000000000================ Library Overview ================ Passlib is a collection of routines for managing password hashes such as found in unix "shadow" files, as returned by stdlib's :func:`!crypt`, as stored in mysql and postgres, and various other places. Passlib's contents can be roughly grouped into three categories: password hashes, password contexts, and utility functions. Password Hashes =============== All of the hash schemes supported by Passlib are implemented 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 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: * :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: * :mod:`passlib.apache` -- classes for managing htpasswd and htdigest files. * :mod:`passlib.ext.django` -- Django plugin which monkeypatches support for (almost) any hash in Passlib. Utility Functions ================= 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. passlib-1.6.5/docs/new_app_quickstart.rst0000644000175000017500000002351012257351267021706 0ustar biscuitbiscuit00000000000000================================ New Application Quickstart Guide ================================ Need to quickly get password hash support added into your new application, and don't have time to wade through pages of documentation, comparing and contrasting all the different schemes? Read on... Really Quick Start ================== The fastest route is to use the preconfigured :data:`~passlib.apps.custom_app_context` object. It supports the :class:`~passlib.hash.sha256_crypt` and :class:`~passlib.hash.sha512_crypt` schemes, and defaults to 40000 hash iterations for increased strength. 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) >>> from passlib.apps import custom_app_context as pwd_context >>> # encrypting a password... >>> hash = pwd_context.encrypt("somepass") >>> # 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: >>> 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 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. Passlib contains a large number of schemes, but most of them should only be used when a specific format is explicitly required. For new applications, there are really only three choices [#choices]_: * :class:`~passlib.hash.bcrypt` * :class:`~passlib.hash.sha512_crypt` * :class:`~passlib.hash.pbkdf2_sha512` All three password hashes share the following properties: * no known vulnerabilities. * based on documented & widely reviewed algorithms. * basic algorithm has seen heavy scrutiny and use for at least 10 years. * public-domain or BSD-licensed reference implementations available. * in use across a number of OSes and/or a wide variety of applications. * variable rounds for configuring flexible cpu cost on a per-hash basis. * at least 96 bits of salt. 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. .. rst-class:: html-toggle Detailed Comparison of Choices ------------------------------ BCrypt ...... :class:`~passlib.hash.bcrypt` is `based `_ on the well-tested Blowfish cipher. In use since 1999, it's the default hash on all BSD variants. If you want your application's hashes to be readable by the native BSD crypt() function, this is the hash to use. There is also an alternative LDAP-formatted version (:class:`~passlib.hash.ldap_bcrypt`) available. 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, 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 (e.g. web apps). .. note:: For BCrypt support on non-BSD systems, 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 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 native Linux crypt() function, this is the hash to use. There is also :class:`~passlib.hash.sha256_crypt`, which may be faster on 32 bit processors; as well as LDAP-formatted versions of these ( :class:`~passlib.hash.ldap_sha512_crypt` and :class:`~passlib.hash.ldap_sha256_crypt`). 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 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. .. 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. .. note:: 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 ...... :class:`~passlib.hash.pbkdf2_sha512` is a custom has format designed for Passlib. However, it directly uses the `PBKDF2 `_ key derivation function, which was standardized in 2000, and found across a `wide variety `_ of applications and platforms. Unlike the previous two hashes, PBKDF2 has a simple and portable design, which is resistant (but not immune) to collision and preimage attacks on the underlying message digest. There is also :class:`~passlib.hash.pbkdf2_sha256`, which may be faster on 32 bit processors; as well as LDAP-formatted versions of these ( :class:`~passlib.hash.ldap_pbkdf2_sha512` and :class:`~passlib.hash.ldap_pbkdf2_sha256`). Issues: PBKDF2 has no security or portability issues. 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 its own :ref:`custom format `). .. note:: Passlib strongly suggests installing the external M2Crypto package to speed up PBKDF2 calculations, though this is not required. .. 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 thoroughly 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... # from passlib.context import CryptContext # # 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. schemes=["pbkdf2_sha256", "des_crypt" ], default="pbkdf2_sha256", # 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, # 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:: >>> # import context from where you defined it... >>> from myapp.model.security import pwd_context >>> # encrypting a password... >>> hash = pwd_context.encrypt("somepass") >>> hash '$pbkdf2-sha256$7252$qKFNyMYTmgQDCFDS.jRJDQ$sms3/EWbs4/3k3aOoid5azwq3HPZKVpUUrAsCfjrN6M' >>> # verifying a password... >>> pwd_context.verify("somepass", hash) True >>> pwd_context.verify("wrongpass", hash) False .. seealso:: * :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 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. passlib-1.6.5/CHANGES0000644000175000017500000007100012560246433015304 0ustar biscuitbiscuit00000000000000.. -*- restructuredtext -*- .. _whats-new: =============== Release History =============== Upcoming in **1.7** =================== .. warning:: The upcoming Passlib 1.7 will make a number of backwards incompatible changes: * **It will require Python 2.6 / 3.2 or newer; dropping support for Python 2.5, 3.0, 3.1** * The :mod:`passlib.ext.django` extension will require Django 1.6 or newer; dropping support for Django 1.5 and earlier. * New hashes generated by :class:`!HtpasswdFile` will use the strongest algorithm available on the host, rather than one that is guaranteed to be portable. Applications can explicitly set ``default_scheme="portable"`` to retain the old behavior (new in Passlib 1.6.3). **1.6.5** (2015-08-04) ====================== Fixed some minor bugs in the test suite which were causing erroneous test failures (:issue:`57` and :issue:`58`). The passlib library itself is unchanged. **1.6.4** (2015-07-25) ====================== This release rolls up assorted bug & compatibility fixes since 1.6.2. Bugfixes -------- * Correctly detect bcrypt 2.0. Previous releases were incorrectly detecting it as py-bcrypt, causing spurious errors (:issue:`56`). * CryptContext now accepts scheme names as unicode (:issue:`54`). * :mod:`passlib.ext.django` now works correctly with Django 1.7-1.8. Previous releases had various test failures (:issue:`52`). * :class:`passlib.apache.HtpasswdFile` now recognizes bcrypt, sha256_crypt, sha512_crypt hashes (:issue:`55`). BCrypt Changes -------------- A few changes have been made to the :class:`~passlib.hash.bcrypt` hash: * It now supports the ``$2b$`` hash format. * It will now issue a :exc:`~passlib.exc.PasslibSecurityWarning` if the active backend is vulnerable to the :ref:`wraparound bug `, and automatically enable a workaround (py-bcrypt is known to be vulnerable as of v0.4). * It will throw a :exc:`~passlib.exc.PasslibSecurityError` if the active backend is vulnerable to the :ref:`8-bit bug ` (none of Passlib's backends are known to be vulnerable as of 2015-07). * Updated documentation to indicate the cffi-based `bcrypt `_ library is now the recommended bcrypt backend. * Backend capability detection code refactored to rely on runtime detection rather than hardcoded information. Other Changes ------------- * Source repo's ``tox.ini`` updated. Now assumes python3 by default, and refactored test environments to more cleanly delineate the different setups being tested. * Passlib releases are now published as wheels instead of eggs. .. note:: Release **1.6.3** was skipped due to upload issues. **1.6.2** (2013-12-26) ====================== Minor changes & compatibility fixes * Re-tuned the :attr:`~passlib.ifc.PasswordHash.default_rounds` values for all of the hashes. * Added the new :doc:`bcrypt_sha256 ` hash, which wraps BCrypt using SHA256 in order to work around BCrypt's password size limitations (:issue:`43`). * :doc:`passlib.hash.bcrypt `: Added support for the `bcrypt `_ library as one of the possible bcrypt backends that will be used if available. (:issue:`49`) * :mod:`passlib.ext.django`: Passlib's Django extension (and it's related hashes and unittests) have been updated to handle some minor API changes in Django 1.5-1.6. They should now be compatible with Django 1.2 and up. (:issue:`50`) **1.6.1** (2012-08-02) ====================== 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 some :mod:`!passlib.apache` unittests to fail if mtime resolution >= 1 second (:issue:`35`). * *bugfix:* Fixed minor bug in :mod:`!passlib.registry`, should now work correctly under Python 3.3. * Various documentation updates and corrections. **1.6** (2012-05-01) ==================== 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 its 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 its 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 its 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: **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:* 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 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)``. This function will clear the padding bits of any BCrypt hashes, and should leave all other strings alone. **1.5.2** (2011-09-19) ====================== Minor bugfix release -- mainly Django-related fixes Hashes .. currentmodule:: passlib.hash * *bugfix:* :class:`django_des_crypt` now accepts all :data:`hash64 ` characters in its salts; previously it accepted only lower-case hexadecimal characters (:issue:`22`). * Additional unittests added for all standard :doc:`Django hashes `. * :class:`django_des_crypt` now rejects hashes where salt and checksum containing mismatched salt characters. CryptContext .. currentmodule:: passlib.context * *bugfix:* fixed exception in :meth:`CryptPolicy.iter_config` that occurred when iterating over deprecation options. * Added documentation for the (mistakenly undocumented) :meth:`CryptContext.verify_and_update` method. **1.5.1** (2011-08-17) ====================== Minor bugfix release -- now compatible with Google App Engine. * *bugfix:* make ``passlib.hash.__loader__`` attribute writable - 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`. * *bugfix:* fixed error thrown by CryptContext.verify 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`. * under GAE, disable all unittests which require writing to filesystem. * more unittest coverage for :mod:`passlib.apps` and :mod:`passlib.hosts`. * improved version datestamps in build script. **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: 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 ------------ .. currentmodule:: passlib.context * 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. 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() convenience 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 unneeded 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. * improved password hash api conformance tests * PyPy compatibility **1.3.1** (2011-03-28) ====================== Minor bugfix release. * bugfix: replaced "sys.maxsize" reference that was failing under py25 * bugfix: fixed default_rounds>max_rounds border case that could cause ValueError during CryptContext.encrypt() * minor documentation changes * added instructions for building html documentation from source **1.3** (2011-03-25) ==================== First public release. * documentation completed * 99% unittest coverage * some refactoring and lots of bugfixes * added support for a number of additional password schemes: bigcrypt, crypt16, sun md5 crypt, nthash, lmhash, oracle10 & 11, phpass, sha1, generic hex digests, ldap digests. **1.2** (2011-01-06) ==================== .. note:: For this and all previous versions, Passlib did not exist independently, but as a subpackage of *BPS*, a private & unreleased toolkit library. * many bugfixes * global registry added * transitional release for applications using BPS library. * first truly functional release since splitting from BPS library (see below). **1.0** (2009-12-11) ==================== * CryptContext & CryptHandler framework * added support for: des-crypt, bcrypt (via py-bcrypt), postgres, mysql * added unit tests **0.5** (2008-05-10) ==================== * initial production version * consolidated from code scattered across multiple applications * MD5-Crypt, SHA256-Crypt, SHA512-Crypt support passlib-1.6.5/LICENSE0000644000175000017500000001061312560246517015324 0ustar biscuitbiscuit00000000000000.. -*- restructuredtext -*- ===================== Copyrights & Licenses ===================== License for Passlib =================== Passlib is (c) `Assurance Technologies `_, and is released under the `BSD license `_:: Passlib Copyright (c) 2008-2015 Assurance Technologies, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Assurance Technologies, nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Licenses for incorporated software ================================== Passlib contains some code derived from the following sources: MD5-Crypt --------- The source file ``passlib/handlers/md5_crypt.py`` contains code derived from the original `FreeBSD md5-crypt implementation `_, which is available under the following license:: "THE BEER-WARE LICENSE" (Revision 42): wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp converted to python May 2008 by Eli Collins DES --- The source file ``passlib/utils/des.py`` contains code derived from `UnixCrypt.java `_, a pure-java implementation of the historic unix-crypt password hash algorithm. It is available under the following license:: UnixCrypt.java 0.9 96/11/25 Copyright (c) 1996 Aki Yoshida. All rights reserved. Permission to use, copy, modify and distribute this software for non-commercial or commercial purposes and without fee is hereby granted provided that this copyright notice appears in all copies. modified April 2001 by Iris Van den Broeke, Daniel Deville modified Aug 2005 by Greg Wilkins (gregw) 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 TORTUOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. passlib-1.6.5/setup.py0000644000175000017500000001410212555044153016022 0ustar biscuitbiscuit00000000000000"""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 py3k = (sys.version_info[0] >= 3) try: from setuptools import setup has_distribute = True except ImportError: from distutils.core import setup has_distribute = False #============================================================================= # init setup options #============================================================================= opts = { "cmdclass": { } } args = sys.argv[1:] #============================================================================= # 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. # XXX: could check for *absence* of PKG-INFO instead if os.path.exists(os.path.join(root_dir, "passlib.komodoproject")): # check for --for-release flag indicating this isn't a snapshot for_release = False i = 0 while i < len(args): v = args[i] if v == '--for-release': for_release = True del args[i] break elif not v.startswith("-"): break i += 1 if for_release: assert '.dev' not in VERSION and '.post' not in VERSION else: # add datestamp if doing a snapshot dstr = time.strftime("%Y%m%d") if VERSION.endswith(".dev0") or VERSION.endswith(".post0"): VERSION = VERSION[:-1] + dstr else: assert '.dev' not in VERSION and '.post' not in VERSION VERSION += ".post" + dstr # subclass build_py & sdist so they rewrite passlib/__init__.py # 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 30 schemes" 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 applications. * See the `documentation `_ for details, installation instructions, and examples. * See the `homepage `_ for the latest news and more information. * See the `changelog `_ for a description of what's new in Passlib. All releases are signed with the gpg key `4CE1ED31 `_. """ KEYWORDS = "password secret hash security crypt md5-crypt \ sha256-crypt sha512-crypt bcrypt apache htpasswd htdigest pbkdf2" CLASSIFIERS = """\ Intended Audience :: Developers License :: OSI Approved :: BSD License Natural Language :: English Operating System :: OS Independent Programming Language :: Python :: 2.5 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") elif '.post' in VERSION: CLASSIFIERS.append("Development Status :: 4 - Beta") else: is_release = True CLASSIFIERS.append("Development Status :: 5 - Production/Stable") #============================================================================= # run setup #============================================================================= # XXX: could omit 'passlib._setup' from eggs, but not sdist setup( # package info packages = [ "passlib", "passlib.ext", "passlib.ext.django", "passlib.handlers", "passlib.tests", "passlib.utils", "passlib.utils._blowfish", "passlib._setup", ], package_data = { "passlib.tests": ["*.cfg"] }, zip_safe=True, # metadata name = "passlib", version = VERSION, author = "Eli Collins", author_email = "elic@assurancetechnologies.com", license = "BSD", url = "https://bitbucket.org/ecollins/passlib", download_url = ("http://pypi.python.org/packages/source/p/passlib/passlib-" + VERSION + ".tar.gz") if is_release else None, description = SUMMARY, long_description = DESCRIPTION, keywords = KEYWORDS, classifiers = CLASSIFIERS, tests_require = 'nose >= 1.1', test_suite = 'nose.collector', # extra opts script_args=args, **opts ) #============================================================================= # eof #============================================================================= passlib-1.6.5/README0000644000175000017500000000510012560150767015173 0ustar biscuitbiscuit00000000000000.. -*- restructuredtext -*- ========================== The Passlib Python Library ========================== Welcome ======= 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 `documentation `_ for details, installation instructions, and examples. * See the `changelog `_ for a description of what's new in Passlib. * Visit `PyPI `_ for the latest stable release. All releases are signed with the gpg key `4CE1ED31 `_. * Additional questions about usage or features? Feel free to post on our `mailing list `_. Usage ===== A quick example of using passlib to integrate into a new application:: >>> # 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... >>> hash = pwd_context.encrypt("somepass") >>> hash '$6$rounds=36122$kzMjVFTjgSVuPoS.$zx2RoZ2TYRHoKn71Y60MFmyqNPxbNnTZdwYD8y2atgoRIp923WJSbcbQc6Af3osdW96MRfwb5Hk7FymOM6D7J1' >>> # verifying a password... >>> ok = pwd_context.verify("somepass", hash) True >>> ok = pwd_context.verify("letmein", hash) False For more details and an extended set of examples, see the full documentation; This example barely touches on the range of features available. Online Resources ================ * Homepage - https://bitbucket.org/ecollins/passlib * Documentation - http://packages.python.org/passlib * Mailing list - http://groups.google.com/group/passlib-users * Downloads - https://pypi.python.org/pypi/passlib * Source - https://bitbucket.org/ecollins/passlib/src * Issues - https://bitbucket.org/ecollins/passlib/issues Source ========= Passlib's source repository uses Mercurial. When building Passlib from an hg clone, note that there are two main branches: ``default`` and ``stable``. * ``default`` is the bleeding edge of the next major release. It may sometimes be of alpha quality. * ``stable`` is the latest released version plus any pending bugfixes, and should be safe to use in production. passlib-1.6.5/passlib.egg-info/0000755000175000017500000000000012560246762017447 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib.egg-info/PKG-INFO0000644000175000017500000000426412560246762020552 0ustar biscuitbiscuit00000000000000Metadata-Version: 1.1 Name: passlib Version: 1.6.5 Summary: comprehensive password hashing framework supporting over 30 schemes Home-page: https://bitbucket.org/ecollins/passlib Author: Eli Collins Author-email: elic@assurancetechnologies.com License: BSD Download-URL: http://pypi.python.org/packages/source/p/passlib/passlib-1.6.5.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 applications. * See the `documentation `_ for details, installation instructions, and examples. * See the `homepage `_ for the latest news and more information. * See the `changelog `_ for a description of what's new in Passlib. All releases are signed with the gpg key `4CE1ED31 `_. Keywords: password secret hash security crypt md5-crypt sha256-crypt sha512-crypt bcrypt apache htpasswd htdigest pbkdf2 Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.5 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 passlib-1.6.5/passlib.egg-info/dependency_links.txt0000644000175000017500000000000112560246762023515 0ustar biscuitbiscuit00000000000000 passlib-1.6.5/passlib.egg-info/top_level.txt0000644000175000017500000000001012560246762022170 0ustar biscuitbiscuit00000000000000passlib passlib-1.6.5/passlib.egg-info/SOURCES.txt0000644000175000017500000001074512560246762021342 0ustar biscuitbiscuit00000000000000CHANGES LICENSE MANIFEST.in README setup.cfg setup.py tox.ini docs/conf.py docs/contents.rst docs/copyright.rst docs/history.rst docs/index.rst docs/install.rst docs/modular_crypt_format.rst docs/new_app_quickstart.rst 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 docs/_static/logo.png docs/_static/logo.svg docs/_static/masthead.png docs/_static/masthead.svg docs/lib/passlib.apache.rst docs/lib/passlib.apps.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.bcrypt_sha256.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 docs/lib/passlib.hash.django_std.rst docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst docs/lib/passlib.hash.fshp.rst docs/lib/passlib.hash.grub_pbkdf2_sha512.rst docs/lib/passlib.hash.hex_digests.rst docs/lib/passlib.hash.ldap_crypt.rst 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 docs/lib/passlib.hash.oracle10.rst docs/lib/passlib.hash.oracle11.rst docs/lib/passlib.hash.pbkdf2_digest.rst docs/lib/passlib.hash.phpass.rst 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_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.handlers.rst docs/lib/passlib.utils.md4.rst docs/lib/passlib.utils.pbkdf2.rst docs/lib/passlib.utils.rst passlib/__init__.py passlib/apache.py passlib/apps.py passlib/context.py passlib/exc.py passlib/hash.py passlib/hosts.py passlib/ifc.py passlib/registry.py passlib/win32.py passlib.egg-info/PKG-INFO passlib.egg-info/SOURCES.txt passlib.egg-info/dependency_links.txt passlib.egg-info/top_level.txt passlib.egg-info/zip-safe passlib/_setup/__init__.py passlib/_setup/docdist.py passlib/_setup/stamp.py passlib/ext/__init__.py passlib/ext/django/__init__.py passlib/ext/django/models.py 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 passlib/handlers/fshp.py passlib/handlers/ldap_digests.py passlib/handlers/md5_crypt.py passlib/handlers/misc.py passlib/handlers/mssql.py passlib/handlers/mysql.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/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_context_deprecated.py passlib/tests/test_ext_django.py passlib/tests/test_handlers.py passlib/tests/test_handlers_bcrypt.py passlib/tests/test_handlers_django.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/handlers.py passlib/utils/md4.py passlib/utils/pbkdf2.py passlib/utils/_blowfish/__init__.py passlib/utils/_blowfish/_gen_files.py passlib/utils/_blowfish/base.py passlib/utils/_blowfish/unrolled.pypasslib-1.6.5/passlib.egg-info/zip-safe0000644000175000017500000000000112214647077021077 0ustar biscuitbiscuit00000000000000 passlib-1.6.5/PKG-INFO0000644000175000017500000000426412560246762015423 0ustar biscuitbiscuit00000000000000Metadata-Version: 1.1 Name: passlib Version: 1.6.5 Summary: comprehensive password hashing framework supporting over 30 schemes Home-page: https://bitbucket.org/ecollins/passlib Author: Eli Collins Author-email: elic@assurancetechnologies.com License: BSD Download-URL: http://pypi.python.org/packages/source/p/passlib/passlib-1.6.5.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 applications. * See the `documentation `_ for details, installation instructions, and examples. * See the `homepage `_ for the latest news and more information. * See the `changelog `_ for a description of what's new in Passlib. All releases are signed with the gpg key `4CE1ED31 `_. Keywords: password secret hash security crypt md5-crypt sha256-crypt sha512-crypt bcrypt apache htpasswd htdigest pbkdf2 Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.5 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 passlib-1.6.5/tox.ini0000644000175000017500000002613312556150507015634 0ustar biscuitbiscuit00000000000000#=========================================================================== # Passlib configuration for TOX #=========================================================================== # #----------------------------------------------------------------------- # config options #----------------------------------------------------------------------- # # PASSLIB_TEST_MODE: # # The default test environment sets PASSLIB_TEST_MODE=full. # If you're wanting to quickly test various under various environments, # you may want to pick another value: # # "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. # #----------------------------------------------------------------------- # vm test environments #----------------------------------------------------------------------- # The default testenv assumes py3, with specific configurations for py26/27. # # Tox is configured by default to run all the test environments, # with the exception of "jython27" and "gae25", which usually require special setup. # #----------------------------------------------------------------------- # external library integration tests #----------------------------------------------------------------------- # There are a few optional libraries, whose integration is tested separately. # Each has it's own section (below) which deals with it's particular requirements: # # * M2Crypto # * BCrypt Backends # * Django # # NOTE: The 'bcrypt' package is tested through the normal VM tests, # and requires libffi-dev (unless e.g. there's a wheel available for your platform). #=========================================================================== #=========================================================================== # global config #=========================================================================== [tox] minversion=1.8 envlist = py{26,27,32,33,34}, pypy,pypy3, m2crypto-py{2,3}, other-bcrypt-py{2,3},wo-bcrypt-py3, django{,14,145,15}-py2, django{,16,17,-wo-bcrypt}-py3, gae-py27 #=========================================================================== # stock CPython VMs # # NOTE: bcrypt requires libffi-dev # # NOTE: '_mindeps' section isn't used by tox -- it's a custom var with just # minimal dependancies required for testing (e.g. no bcrypt, m2crypto, etc), # used as shared config var by a bunch of other tests below. #=========================================================================== # default (python 3) configuration [testenv] setenv = PASSLIB_TEST_MODE = full changedir = {envdir} commands = nosetests {posargs:--randomize passlib.tests} _mindeps = nose coverage randomize unittest2py3k deps = {[testenv]_mindeps} bcrypt [testenv:py27] _mindeps = # NOTE: this is custom var with just minimal dependancies required for test # (e.g. no bcrypt, m2crypto, etc) nose coverage randomize unittest2 deps = {[testenv:py27]_mindeps} bcrypt [testenv:py26] deps = {[testenv:py27]deps} #=========================================================================== # M2Crypto accel testing # # Passlib uses M2Crypto to accelerate certain PBKDF2 calls, # Since those calls reasonably widespread, the M2Crypto test envs # run the full suite of passlib tests. # # NOTE: M2Crypto requires python-dev, swig, & libssl-dev #=========================================================================== # TODO: restrict these down to just the tests M2Crypto affects. [testenv:m2crypto-py2] basepython = python2 deps = {[testenv:py27]deps} M2Crypto [testenv:m2crypto-py3] basepython = python3 deps = {[testenv]deps} M2Crypto #=========================================================================== # bcrypt backend testing # # bcrypt -- https://pypi.python.org/pypi/bcrypt # py-bcrypt -- https://pypi.python.org/pypi/py-bcrypt/ # bcryptor - https://pypi.python.org/pypi/Bcryptor # # 'bcrypt' is the recommended bcrypt library for passlib, # and is tested under the various VM-specific test envs (py27, py31, etc) # the remainder are tested below. # # NOTE: bcrypt requires libffi-dev # NOTE: bcryptor requires python-dev & Cython # NOTE: bcryptor isn't py3 compatible, so only tested under py2 #=========================================================================== # note: used this just to check legacy issues, not generally needed. ;[testenv:bcrypt1.0-py2] ;basepython = python2 ;deps = ; {[testenv:py27]_mindeps} ; bcrypt<1.1 ;commands = ; nosetests {posargs:--randomize passlib.tests.test_handlers_bcrypt} [testenv:other-bcrypt-py2] basepython = python2 deps = {[testenv:py27]_mindeps} bcryptor py-bcrypt commands = nosetests {posargs:--randomize passlib.tests.test_handlers_bcrypt} [testenv:other-bcrypt-py3] basepython = python3 deps = {[testenv]_mindeps} py-bcrypt commands = nosetests {posargs:--randomize passlib.tests.test_handlers_bcrypt} #--------------------------------------------------------------------- # the "wo-bcrypt" environments makes sure code is ok w/o # any bcrypt library installed. # # NOTE: running *all* tests, not just bcrypt ones -- # had a few registry related tests fail when bcrypt is missing. #--------------------------------------------------------------------- [testenv:wo-bcrypt-py2] setenv = PASSLIB_TEST_MODE = quick basepython = python2 deps = {[testenv:py27]_mindeps} commands = nosetests {posargs:--randomize passlib.tests} [testenv:wo-bcrypt-py3] setenv = PASSLIB_TEST_MODE = quick basepython = python3 deps = {[testenv]_mindeps} commands = nosetests {posargs:--randomize passlib.tests} #=========================================================================== # Django integration testing # # currently supports Django 1.4 + # # there are tests for the major django versions at the time of release, # short the latest version, which is handled by the 'django' test. # # Django 1.4 / 1.5 are only tested under python 2. # Django 1.6 + are only tested under python 3, # with the exception of the latest version, which is tested under both. # # All django releases are testing with bcrypt installed, # there is special test which runs w/o bcrypt. # # NOTE: django doesn't seem to work well with py-bcrypt or bcryptor, # so those backends aren't tested. #=========================================================================== #--------------------------------------------------------------------- # legacy django versions that are py2 only #--------------------------------------------------------------------- [testenv:django14-py2] basepython = python2 deps = {[testenv:py27]deps} django<1.5 commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} # NOTE: testing 1.4.5 specifically to detect regression of issue 52, since django < 1.4.6 # has some slight differences in the hasher classes & tests. [testenv:django145-py2] basepython = python2 deps = {[testenv:py27]deps} django==1.4.5 commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} [testenv:django15-py2] basepython = python2 deps = # NOTE: would use py27 deps, but django 1.5 has issue with bcrypt 2.0 {[testenv:py27]_mindeps} py-bcrypt django<1.6 commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} #--------------------------------------------------------------------- # legacy django verions that are py2/py3 #--------------------------------------------------------------------- [testenv:django16-py3] basepython = python3 deps = {[testenv]deps} django<1.7 commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} [testenv:django17-py3] basepython = python3 deps = {[testenv]deps} django<1.8 commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} #--------------------------------------------------------------------- # latest django version #--------------------------------------------------------------------- # NOTE: django >= 1.7 distributes tests as part of source, not the package, # so for full integration tests to run, have a copy of the latest django source, # and set the env var PASSLIB_TESTS_DJANGO_SOURCE_PATH to point to it. [testenv:django-py2] basepython = python2 deps = {[testenv:py27]deps} django passenv = PASSLIB_TESTS_DJANGO_SOURCE_PATH commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} [testenv:django-py3] basepython = python3 deps = {[testenv]deps} django passenv = PASSLIB_TESTS_DJANGO_SOURCE_PATH commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} #--------------------------------------------------------------------- # make sure we're ok w/ django but w/o bcrypt #--------------------------------------------------------------------- [testenv:django-wo-bcrypt-py3] basepython = python3 deps = {[testenv:wo-bcrypt-py3]deps} django commands = nosetests {posargs:--randomize passlib.tests.test_ext_django passlib.tests.test_handlers_django} #=========================================================================== # PyPy VM # NOTE: PyPy 2.4.0 targets Python 2.7 & Python 3.2 #=========================================================================== [testenv:pypy] basepython = pypy deps = {[testenv:py27]deps} [testenv:pypy3] basepython = pypy3 #=========================================================================== # Jython VM # # NOTE: not run by default #=========================================================================== [testenv:jython27] basepython = jython2.7 #=========================================================================== # Google App Engine integration # # NOTE: for this to work, the GAE SDK should be installed in # /usr/local/google_appengine, or set nosegae's --gae-lib-root # # NOTE: not run by default #=========================================================================== [testenv:gae-py27] basepython = python2.7 deps = nose nosegae 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 # run tests nosetests --with-gae {posargs:passlib/tests} #=========================================================================== # eof #=========================================================================== passlib-1.6.5/MANIFEST.in0000644000175000017500000000013612555044153016050 0ustar biscuitbiscuit00000000000000recursive-include docs * include LICENSE README CHANGES passlib/tests/*.cfg tox.ini setup.cfg passlib-1.6.5/passlib/0000755000175000017500000000000012560246762015755 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/__init__.py0000644000175000017500000000012712560245022020052 0ustar biscuitbiscuit00000000000000"""passlib - suite of password hashing & generation routines""" __version__ = '1.6.5' passlib-1.6.5/passlib/ifc.py0000644000175000017500000001730112555044153017064 0ustar biscuitbiscuit00000000000000"""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 #============================================================================= passlib-1.6.5/passlib/context.py0000644000175000017500000033531312555044153020015 0ustar biscuitbiscuit00000000000000"""passlib.context - CryptContext implementation""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from functools import update_wrapper import inspect import re import hashlib from math import log as logb, ceil import logging; log = logging.getLogger(__name__) import os import re from time import sleep from warnings import warn # 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, native_string_types # local __all__ = [ 'CryptContext', 'LazyCryptContext', 'CryptPolicy', ] #============================================================================= # 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. ") class CryptPolicy(object): """ .. deprecated:: 1.6 This class has been deprecated, and will be removed in Passlib 1.8. All of its 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: .. automethod:: from_path .. automethod:: from_string .. automethod:: from_source .. automethod:: from_sources .. automethod:: replace 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 .. automethod:: get_handler .. automethod:: get_options .. automethod:: handler_is_deprecated .. automethod:: get_min_verify_time Exporting ========= .. automethod:: iter_config .. automethod:: to_dict .. automethod:: to_file .. automethod:: to_string .. note:: CryptPolicy are immutable. Use the :meth:`replace` method to mutate existing instances. .. deprecated:: 1.6 """ #=================================================================== # class methods #=================================================================== @classmethod def from_path(cls, path, section="passlib", encoding="utf-8"): """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)) @classmethod def from_string(cls, source, section="passlib", encoding="utf-8"): """create a CryptPolicy instance from a string. .. deprecated:: 1.6 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. """ 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)) @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: return cls(_internal_context=CryptContext.from_path(source)) @classmethod def from_sources(cls, sources, _warn=True): """create a CryptPolicy instance by merging multiple sources. each source is interpreted as by :meth:`from_source`, and the results are merged together. .. 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)) 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: 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. .. 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: return self._context.handler(name, category) except KeyError: if required: raise else: return None def get_min_verify_time(self, category=None): """get min_verify_time setting for policy. .. 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 def get_options(self, name, category=None): """return dictionary of options specific to a given handler. .. 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] def handler_is_deprecated(self, name, category=None): """check if handler has been deprecated by policy. .. 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 + "``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) #=================================================================== # serialization #=================================================================== def iter_config(self, ini=False, resolve=False): """iterate over key/value pairs representing the policy object. .. 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: 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) ) def to_dict(self, resolve=False): """export policy object as dictionary of options. .. 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) def to_string(self, section="passlib", encoding=None): """export policy to file. .. deprecated:: 1.6 applications should use :meth:`CryptContext.to_string` instead. """ 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 #=================================================================== # eoc #=================================================================== #============================================================================= # _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. """ #=================================================================== # instance attrs #=================================================================== # 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 # encrypt()/genconfig() attrs settings = None # options to be passed directly to encrypt() # verify() attrs _min_verify_time = None # 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 #=================================================================== # 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 #=================================================================== # 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,) def __repr__(self): # pragma: no cover -- debugging return "<_CryptRecord 0x%x for %s>" % (id(self), self._errprefix) #=================================================================== # 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) #---------------------------------------------------- # 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<= %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``. # # 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: 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 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 its 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, native_string_types): 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, native_string_types): handler = get_crypt_handler(elem) scheme = handler.name else: 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 handlers.append(handler) schemes.append(scheme) 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: category_map[cat] = value # 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, native_string_types): 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, native_string_types): 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, native_string_types): 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, native_string_types): 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. .. warning:: treat return value as readonly! """ return self._context_options.get(key, _default) 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 .. warning:: treat return value as readonly! """ try: return self._scheme_options[scheme][category] except KeyError: return default 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 return kwds, has_cat_options #=================================================================== # deprecated & default schemes #=================================================================== def _init_default_schemes(self): """initialize maps containing default scheme for each category. 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 #=================================================================== # 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 its 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. the options dict includes all the scheme-specific settings, as well as optional *deprecated* and *min_verify_time* keywords. """ # get scheme options kwds, has_cat_options = self.get_scheme_options_with_flag(scheme, 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 self._records[scheme, category] except KeyError: pass # type check if category is not None and not isinstance(category, native_string_types): 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, native_string_types): 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 # scheme not found in configuration for default category raise KeyError("crypt algorithm not found in policy: %r" % (scheme,)) def _get_record_list(self, category=None): """return list of records for category (cached) 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: raise ValueError("hash could not be identified") #=================================================================== # serialization #=================================================================== def iter_config(self, resolve=False): """regenerate original config. 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). 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: 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. """ 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" .. versionadded:: 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: 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. # # 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") policy = property(_get_policy, _set_policy, doc="[deprecated] returns CryptPolicy instance " "tied to this CryptContext") #=================================================================== # 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)) def load_path(self, path, update=False, section="passlib", encoding="utf-8"): """Load new configuration into CryptContext from a local file. This function is a wrapper for :meth:`load` which loads a configuration string from the local file *path*, instead of an in-memory source. Its behavior and options are otherwise identical to :meth:`!load` when provided with an INI-formatted string. .. 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 load(self, source, update=False, section="passlib", encoding="utf-8"): """Load new configuration into CryptContext, replacing existing config. :arg source: source of new configuration to load. this value can be a number of different types: * a :class:`!dict` object, or compatible Mapping the key/value pairs will be interpreted the same keywords for the :class:`CryptContext` class constructor. * a :class:`!unicode` or :class:`!bytes` string this will be interpreted as an INI-formatted file, and appropriate key/value pairs will be loaded from the specified *section*. * another :class:`!CryptContext` object. this will export a snapshot of its configuration using :meth:`to_dict`. :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. :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. :type encoding: str :param encoding: Encoding to use when decode bytes from string. Defaults to ``"utf-8"``. Ignoring when loading from a dictionary. :raises TypeError: * If the source cannot be identified. * If an unknown / malformed keyword is encountered. :raises ValueError: If an invalid keyword value is encountered. .. 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, native_string_types) 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? #=================================================================== # reading configuration #=================================================================== 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)) #=================================================================== # exporting config #=================================================================== @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 @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) assert isinstance(value, native_string_types), \ "expected string for key: %r %r" % (key, value) # escape any percent signs. return value.replace("%", "%%") def to_dict(self, resolve=False): """Return current configuration as a dictionary. :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. """ # 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)) 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) def to_string(self, section="passlib"): """serialize to INI format and return as unicode string. :param section: name of INI section to output, defaults to ``"passlib"``. :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 #=================================================================== # 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 retrieved # 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: Optional secret associated with the provided ``hash``. This is not required, or even currently used for anything... it's for forward-compatibility with any future update checks that might need this information. If provided, Passlib assumes the secret has already been verified successfully against the hash. .. versionadded:: 1.6 :returns: ``True`` if hash should be replaced, otherwise ``False``. :raises ValueError: If the hash did not match any of the configured :meth:`schemes`. .. versionadded:: 1.6 This method was previously named :meth:`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="2.0", replacement="CryptContext.needs_update()") def hash_needs_update(self, hash, scheme=None, category=None): """Legacy alias for :meth:`needs_update`. .. deprecated:: 1.6 This method was renamed to :meth:`!needs_update` in version 1.6. This alias will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.5. """ return self.needs_update(hash, scheme, category) def genconfig(self, scheme=None, category=None, **settings): """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: 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 \*\*settings: All additional keywords are passed to the appropriate handler, and should match its :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`. """ return self._get_record(scheme, category).genconfig(**settings) def genhash(self, secret, config, scheme=None, category=None, **kwds): """Generate hash for the specified secret using another hash. 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 its :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*. """ # 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 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 hash handler itself, instead of the name of the hash. :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. """ record = self._identify_record(hash, category, required) if record is None: return None elif resolve: return record.handler else: return record.scheme def encrypt(self, secret, scheme=None, category=None, **kwds): """run secret through selected algorithm, returning resulting hash. :type secret: unicode or bytes :arg secret: the password to hash. :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, 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 selected algorithm's :meth:`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. This includes any keywords passed to the underlying hash's :meth:`PasswordHash.encrypt() ` method. .. seealso:: the :ref:`context-basic-example` example in the tutorial """ # 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 :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). :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. :param \*\*kwds: All additional keywords are passed to the appropriate handler, and should match its :attr:`~passlib.ifc.PasswordHash.context_kwds`. :returns: ``True`` if the password matched the hash, else ``False``. :raises ValueError: * if the hash did not match any of the configured :meth:`schemes`. * if any of the arguments have an invalid value (this includes any keywords passed to the underlying hash's :meth:`PasswordHash.verify() ` method). :raises TypeError: * if any of the arguments have an invalid type (this includes any keywords passed to the underlying hash's :meth:`PasswordHash.verify() ` method). .. 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 password and re-hash the password if needed, all in a single call. 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: 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 :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 that hash's :attr:`PasswordHash.context_kwds `. :returns: This function returns a tuple containing two elements: ``(verified, replacement_hash)``. The first is a boolean flag indicating whether the password verified, and the second an optional replacement hash. The tuple 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 updating. * ``(True, str)`` indicates the secret verified correctly, but the current hash needs to be updated. The :class:`!str` will be the freshly generated hash, to replace the old one. :raises TypeError, ValueError: For the same reasons as :meth:`verify`. .. seealso:: the :ref:`context-migration-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. record = self._get_or_identify_record(hash, scheme, category) if not record.verify(secret, hash, **kwds): return False, None 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 #=================================================================== class LazyCryptContext(CryptContext): """CryptContext subclass which doesn't load handlers until needed. This is a subclass of CryptContext which takes in a set of arguments exactly like CryptContext, but won't import any handlers (or even parse its arguments) until the first time one of its methods is accessed. :arg schemes: The first positional argument can be a list of schemes, or omitted, just like CryptContext. :param onload: If a callable is passed in via this keyword, it will be invoked at lazy-load time with the following signature: ``onload(**kwds) -> kwds``; where ``kwds`` is all the additional kwds passed to LazyCryptContext. 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 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:`!onload()` to provide dynamic configuration at *application-run* time. .. note:: This class is only useful if you're referencing handler objects by name, and don't want them imported until runtime. If you want to have the config validated before your application runs, or are passing in already-imported handler instances, you should use :class:`CryptContext` instead. .. versionadded:: 1.4 """ _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 self._lazy_kwds = kwds def _lazy_init(self): kwds = 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") 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 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 #============================================================================= passlib-1.6.5/passlib/utils/0000755000175000017500000000000012560246762017115 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/utils/md4.py0000644000175000017500000001723612555044153020156 0ustar biscuitbiscuit00000000000000""" helper implementing insecure and obsolete md4 algorithm. used for NTHASH format, which is also insecure and broken, since it's just md4(password) implementated based on rfc at http://www.faqs.org/rfcs/rfc1320.html """ #============================================================================= # imports #============================================================================= # core from binascii import hexlify import struct from warnings import warn # site from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY3 # local __all__ = [ "md4" ] #============================================================================= # utils #============================================================================= def F(x,y,z): return (x&y) | ((~x) & z) def G(x,y,z): return (x&y) | (x&z) | (y&z) ##def H(x,y,z): ## return x ^ y ^ z MASK_32 = 2**32-1 #============================================================================= # main class #============================================================================= class md4(object): """pep-247 compatible implementation of MD4 hash algorithm .. attribute:: digest_size size of md4 digest in bytes (16 bytes) .. method:: update update digest by appending additional content .. method:: copy create clone of digest object, including current state .. method:: digest return bytes representing md4 digest of current content .. method:: hexdigest return hexadecimal 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. 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 def __init__(self, content=None): self._count = 0 self._state = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] self._buf = b('') if content: self.update(content) # round 1 table - [abcd k s] _round1 = [ [0,1,2,3, 0,3], [3,0,1,2, 1,7], [2,3,0,1, 2,11], [1,2,3,0, 3,19], [0,1,2,3, 4,3], [3,0,1,2, 5,7], [2,3,0,1, 6,11], [1,2,3,0, 7,19], [0,1,2,3, 8,3], [3,0,1,2, 9,7], [2,3,0,1, 10,11], [1,2,3,0, 11,19], [0,1,2,3, 12,3], [3,0,1,2, 13,7], [2,3,0,1, 14,11], [1,2,3,0, 15,19], ] # round 2 table - [abcd k s] _round2 = [ [0,1,2,3, 0,3], [3,0,1,2, 4,5], [2,3,0,1, 8,9], [1,2,3,0, 12,13], [0,1,2,3, 1,3], [3,0,1,2, 5,5], [2,3,0,1, 9,9], [1,2,3,0, 13,13], [0,1,2,3, 2,3], [3,0,1,2, 6,5], [2,3,0,1, 10,9], [1,2,3,0, 14,13], [0,1,2,3, 3,3], [3,0,1,2, 7,5], [2,3,0,1, 11,9], [1,2,3,0, 15,13], ] # round 3 table - [abcd k s] _round3 = [ [0,1,2,3, 0,3], [3,0,1,2, 8,9], [2,3,0,1, 4,11], [1,2,3,0, 12,15], [0,1,2,3, 2,3], [3,0,1,2, 10,9], [2,3,0,1, 6,11], [1,2,3,0, 14,15], [0,1,2,3, 1,3], [3,0,1,2, 9,9], [2,3,0,1, 5,11], [1,2,3,0, 13,15], [0,1,2,3, 3,3], [3,0,1,2, 11,9], [2,3,0,1, 7,11], [1,2,3,0, 15,15], ] def _process(self, block): """process 64 byte block""" # unpack block into 16 32-bit ints X = struct.unpack("<16I", block) # clone state orig = self._state state = list(orig) # 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 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 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 irange(4): orig[i] = (orig[i]+state[i]) & MASK_32 def update(self, content): if not isinstance(content, bytes): raise TypeError("expected bytes") buf = self._buf if buf: content = buf + content idx = 0 end = len(content) while True: next = idx + 64 if next <= end: self._process(content[idx:next]) self._count += 1 idx = next else: self._buf = content[idx:] return def copy(self): other = _builtin_md4() other._count = self._count other._state = list(self._state) other._buf = self._buf 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) orig = list(self._state) # 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 msglen = self._count*512 + len(buf)*8 block = buf + b('\x80') + b('\x00') * ((119-len(buf)) % 64) + \ struct.pack("<2I", msglen & MASK_32, (msglen>>32) & MASK_32) if len(block) == 128: self._process(block[:64]) self._process(block[64:]) else: assert len(block) == 64 self._process(block) # render digest & restore un-finalized state out = struct.pack("<4I", *self._state) self._state = orig return out def hexdigest(self): return bascii_to_str(hexlify(self.digest())) #=================================================================== # eoc #=================================================================== # keep ref around for unittest, 'md4' usually replaced by ssl wrapper, below. _builtin_md4 = md4 #============================================================================= # check if hashlib provides accelarated md4 #============================================================================= import hashlib from passlib.utils import PYPY def _has_native_md4(): # pragma: no cover -- runtime detection try: h = hashlib.new("md4") except ValueError: # not supported - ssl probably missing (e.g. ironpython) return False result = h.hexdigest() if result == '31d6cfe0d16ae931b73c59d7e0c089c0': return True if PYPY and result == '': # workaround for https://bugs.pypy.org/issue957, fixed in PyPy 1.8 return False # 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 def md4(content=None): """wrapper for hashlib.new('md4')""" return hashlib.new('md4', content or b('')) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/utils/__init__.py0000644000175000017500000016235012555044153021227 0ustar biscuitbiscuit00000000000000"""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 functools import update_wrapper import logging; log = logging.getLogger(__name__) 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 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__ = [ # constants 'PYPY', 'JYTHON', 'sys_bits', 'unix_crypt_schemes', 'rounds_cost_values', # decorators "classproperty", ## "deprecated_function", ## "relocated_function", ## "memoized_class_property", # unicode helpers 'consteq', 'saslprep', # bytes helpers "xor_bytes", "render_bytes", # encoding helpers 'is_same_codec', 'is_ascii_safe', 'to_bytes', 'to_unicode', 'to_native_str', # 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', # randomness 'rng', 'getrandbytes', 'getrandstr', 'generate_password', # object type / interface tests 'is_crypt_handler', 'is_crypt_context', 'has_rounds_info', 'has_salt_info', ] #============================================================================= # 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 hashes algs supported by crypt() on at least one OS. unix_crypt_schemes = [ "sha512_crypt", "sha256_crypt", "sha1_crypt", "bcrypt", "md5_crypt", # "bsd_nthash", "bsdi_crypt", "des_crypt", ] # list of rounds_cost constants rounds_cost_values = [ "linear", "log2" ] # legacy import, will be removed in 1.8 from passlib.exc import MissingBackendError # 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)""" def __init__(self, func): self.im_func = func def __get__(self, obj, cls): return self.im_func(cls) @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__ and ".. deprecated::" not in 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) 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): 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 ##class memoized_class_property(object): ## """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 ## ## def __get__(self, obj, cls): ## func = self.im_func ## value = func(cls) ## setattr(cls, func.__name__, value) ## return value ## ## @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 its 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*. .. 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") # 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 # TODO: use izip instead (but first verify it's faster than zip for this case) 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 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 strings 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``. Properly internationalized applications should run user passwords through this function before hashing. :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. :raises ValueError: if any characters forbidden by the SASLPrep profile are encountered. :returns: normalized unicode string .. note:: This function is not available under Jython, as the Jython stdlib is missing the :mod:`!stringprep` module (`Jython issue 1758320 `_). .. versionadded:: 1.6 """ # 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) ) # 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 # 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 chars 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. 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``. Calling ``render_bytes(source, *args)`` should function roughly the same as ``source % args`` under Python 2. """ if isinstance(source, bytes): 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 isinstance(source, unicode): return source.encode(encoding) else: raise ExpectedStringError(source, param) def to_unicode(source, encoding="utf-8", param="value"): """Helper to normalize input to unicode. :arg source: source bytes/unicode to process. :arg encoding: encoding to use when decoding bytes instances. :param param: optional name of variable/noun to reference when raising errors. :raises TypeError: if source is not unicode or bytes. :returns: * returns unicode strings unchanged. * returns bytes strings decoded using *encoding* """ assert encoding if isinstance(source, unicode): return source elif isinstance(source, bytes): return source.decode(encoding) else: 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) 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: leaves unicode alone, decodes bytes using specified encoding. :raises TypeError: if source is not unicode or bytes. :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 """) @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 its 6-bit value. :param big: Whether the encoding should be big-endian (default False). .. note:: This class does not currently handle base64's padding characters in any way what so ever. Raw Bytes <-> Encoded Bytes =========================== The following methods convert between raw bytes, and strings encoded using the engine's specific base64 variant: .. automethod:: encode_bytes .. automethod:: decode_bytes .. automethod:: encode_transposed_bytes .. automethod:: decode_transposed_bytes .. .. automethod:: check_repair_unused .. automethod:: repair_unused 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. .. automethod:: encode_int6 .. automethod:: decode_int6 .. automethod:: encode_int12 .. automethod:: decode_int12 .. automethod:: encode_int24 .. automethod:: decode_int24 .. automethod:: encode_int64 .. automethod:: decode_int64 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. """ #=================================================================== # 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 # 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. :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 ab64_encode(data): """encode using variant of base64 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 by Passlib's custom pbkdf2 hashes. """ return b64encode(data, _A64_ALTCHARS).strip(_A64_STRIP) def ab64_decode(data): """decode using variant of base64 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 by Passlib's custom pbkdf2 hashes. """ off = len(data) & 3 if off == 0: return b64decode(data, _A64_ALTCHARS) elif off == 2: return b64decode(data + _A64_PAD2, _A64_ALTCHARS) elif off == 3: return b64decode(data + _A64_PAD1, _A64_ALTCHARS) else: # off == 1 raise ValueError("invalid base64 input") #============================================================================= # host OS helpers #============================================================================= try: from crypt import crypt as _crypt except ImportError: # pragma: no cover _crypt = None 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 (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 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 has_urandom = False def genseed(value=None): """generate prng seed value from system resources""" from hashlib import sha512 text = u("%s %s %s %s %.15f %.15f %s") % ( # if caller specified a seed value, mix it in value, # if caller's seed value was an RNG, mix in bits from its state value.getrandbits(1<<15) if hasattr(value, "getrandbits") else None, # add current process id # NOTE: not available in some environments, e.g. GAE os.getpid() if hasattr(os, "getpid") else None, # id of a freshly created object. # (at least 1 byte of which should be hard to predict) id(object()), # the current time, to whatever precision os uses time.time(), time.clock(), # 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/long return int(sha512(text.encode("utf-8")).hexdigest(), 16) if has_urandom: rng = random.SystemRandom() 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 ###just in case rng provides this... ##meth = getattr(rng, "getrandbytes", None) ##if meth: ## return meth(count) if not count: return _BEMPTY def helper(): # XXX: break into chunks for large number of bits? value = rng.getrandbits(count<<3) i = 0 while i < count: yield value & 0xff value >>= 3 i += 1 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""" # 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) if letters == 0: raise ValueError("alphabet must not be empty") if letters == 1: return charset * count # get random value, and write out to buffer def helper(): # XXX: break into chunks for large number of letters? value = rng.randrange(0, letters**count) i = 0 while i < count: yield charset[value % letters] value //= letters i += 1 if isinstance(charset, unicode): return join_unicode(helper()) else: return join_byte_elems(helper()) _52charset = '2346789ABCDEFGHJKMNPQRTUVWXYZabcdefghjkmnpqrstuvwxyz' def generate_password(size=10, charset=_52charset): """generate random password using given length & charset :param size: size of password. :param charset: optional string specified set of characters to draw from. the default charset contains all normal alphanumeric characters, except for the characters ``1IiLl0OoS5``, which were omitted due to their visual similarity. :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) #============================================================================= # 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 #============================================================================= passlib-1.6.5/passlib/utils/_blowfish/0000755000175000017500000000000012560246762021071 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/utils/_blowfish/__init__.py0000644000175000017500000001440012555044153023173 0ustar biscuitbiscuit00000000000000"""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 usable, but is an order of magnitude too slow to be usable 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 usable *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, native_string_types 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, native_string_types) add_null_padding = True if ident == u('2a') or ident == u('2y') or ident == u('2b'): pass elif ident == u('2'): add_null_padding = False elif ident == u('2x'): raise ValueError("crypt_blowfish's buggy '2x' hashes are not " "currently supported") 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 add_null_padding: 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 (72 bytes total). pass_words = engine.key_to_words(password) salt_words = engine.key_to_words(salt) # truncate salt_words to original 16 byte salt, or loop won't wrap # correctly when passed to .eks_salted_expand() salt_words16 = salt_words[:4] # do EKS key schedule setup engine.eks_salted_expand(pass_words, salt_words16) # apply password & salt keys to key schedule a bunch more times. rounds = 1< 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_salted_expand(self, key_words, salt_words): """perform EKS' salted 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_repeated_expand(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 #============================================================================= passlib-1.6.5/passlib/utils/_blowfish/unrolled.py0000644000175000017500000011043712214647077023275 0ustar biscuitbiscuit00000000000000"""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 #============================================================================= passlib-1.6.5/passlib/utils/_blowfish/_gen_files.py0000644000175000017500000001403512257351267023540 0ustar biscuitbiscuit00000000000000"""passlib.utils._blowfish._gen_files - meta script that generates unrolled.py""" #============================================================================= # imports #============================================================================= # core import os import textwrap # pkg from passlib.utils.compat import irange # local #============================================================================= # helpers #============================================================================= def varlist(name, count): return ", ".join(name + str(x) for x in irange(count)) def indent_block(block, padding): """ident block of text""" lines = block.split("\n") return "\n".join( padding + line if line else "" for line in lines ) BFSTR = """\ ((((S0[l >> 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 #============================================================================= passlib-1.6.5/passlib/utils/handlers.py0000644000175000017500000017224012555044153021267 0ustar biscuitbiscuit00000000000000"""passlib.handler - code for implementing handlers, and global registry for handlers""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import inspect import re import hashlib import logging; log = logging.getLogger(__name__) import time import os from warnings import warn # 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 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 'GenericHandler', 'StaticHandler', 'HasUserContext', 'HasRawChecksum', 'HasManyIdents', 'HasSalt', 'HasRawSalt', 'HasRounds', 'HasManyBackends', # other helpers 'PrefixWrapper', ] #============================================================================= # 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 #============================================================================= # 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: return hash.decode("utf-8") except UnicodeDecodeError: 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. :returns: a ``(salt, chk | None)`` tuple. """ # detect prefix hash = to_unicode(hash, "ascii", "hash") assert isinstance(prefix, unicode) if not hash.startswith(prefix): 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 return salt, chk or None elif len(parts) == 1: return parts[0], None else: raise exc.MalformedHashError(handler) 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) if not hash.startswith(prefix): 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 elif len(parts) == 2: rounds, salt = parts chk = None else: raise exc.MalformedHashError(handler) # 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: rounds = default_rounds # return result return rounds, salt, chk or 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 ``$``) :returns: config or hash (native str) """ if checksum: parts = [ident, salt, sep, checksum] else: parts = [ident, salt] return uascii_to_str(join_unicode(parts)) def render_mc3(ident, rounds, salt, checksum, sep=u("$"), rounds_base=10): """format hash using 3-part modular crypt format; inverse of parse_mc3() returns native string with format :samp:`{ident}[{rounds}$]{salt}[${checksum}]`, such as used by sha1_crypt. :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) :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)) #============================================================================= # 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 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:`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 ================ .. attribute:: ident [optional] If this attribute is filled in, the default :meth:`identify` method will use it as a identifying prefix that can be used to recognize instances of this handler's hash. Filling this out is recommended for speed. 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] Specifies the number of characters that should be expected in the checksum string. If omitted, no check will be performed. .. attribute:: checksum_chars [optional] A string listing all the characters allowed in the checksum string. If omitted, no check will be performed. 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 provided to the constructor (after passing it through :meth:`_norm_checksum`). Required Subclass Methods ========================= The following methods must be provided by handler subclass: .. automethod:: from_string .. automethod:: to_string .. automethod:: _calc_checksum 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:: genconfig .. automethod:: genhash .. automethod:: identify .. automethod:: encrypt .. automethod:: verify """ #=================================================================== # 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 = () # optional prefix that uniquely identifies hash ident = None # optional regexp for recognizing hashes, # used by default identify() if .ident isn't specified. _hash_regex = None # if specified, _norm_checksum will require this length checksum_size = None # if specified, _norm_checksum() will validate this checksum_chars = None # 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: return None # 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 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 #=================================================================== @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 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 is not None: return hash.startswith(ident) # 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, **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: hash parsed into components, for formatting / calculating checksum. """ raise NotImplementedError("%s must implement from_string()" % (cls,)) def to_string(self): # pragma: no cover """render instance to hash or configuration string :returns: if :attr:`checksum` is set, should return full hash string. if not, should either return abbreviated configuration string, or fill in a stub checksum. 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: # # withchk=True -- if false, omit checksum portion of hash # raise NotImplementedError("%s must implement from_string()" % (self.__class__,)) ##def to_config_string(self): ## "helper for generating configuration string (ignoring hash)" ## 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(use_defaults=True, **settings).to_string() @classmethod 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 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, **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, **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. its 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) # 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 @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. All that is required by subclasses is an implementation of the :meth:`_calc_checksum` method. """ # TODO: document _norm_hash() 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 .. todo:: document this class's usage """ # NOTE: GenericHandler.checksum_chars is ignored by this implementation. # 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 For the hashes which may use multiple identifier prefixes, this mixin adds an ``ident`` keyword to constructor. Any value provided is passed through the :meth:`norm_idents` method, which takes care of validating the identifier, as well as allowing aliases for easier specification of the identifiers by the user. .. todo:: 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 #=================================================================== ident = None #=================================================================== # 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 if isinstance(ident, bytes): ident = ident.decode('ascii') # check if identifier is valid iv = self.ident_values if ident in iv: return ident # resolve aliases, and recheck against ident_values ia = self.ident_aliases if ia: try: value = ia[ident] except KeyError: pass else: if value in iv: return value # failure! raise ValueError("invalid ident: %r" % (ident,)) #=================================================================== # password hash api #=================================================================== @classmethod def identify(cls, hash): hash = to_unicode_for_identify(hash) return any(hash.startswith(ident) for ident in cls.ident_values) @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, 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`. Class Attributes ================ In order for :meth:`!_norm_salt` to do its job, the following attributes should be provided by the handler subclass: .. attribute:: min_salt_size The minimum number of characters allowed in a salt string. An :exc:`ValueError` will be throw if the provided salt is too small. Defaults to ``None``, for no minimum. .. attribute:: max_salt_size The maximum number of characters allowed in a salt string. 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 [required] If no salt is provided, this should specify the size of the salt that will be generated by :meth:`_generate_salt`. By default this will fall back to :attr:`max_salt_size`. .. attribute:: 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 [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. Instance Attributes =================== .. attribute:: salt This instance attribute will be filled in with the salt provided to the constructor (as adapted by :meth:`_norm_salt`) Subclassable Methods ==================== .. automethod:: _norm_salt .. automethod:: _generate_salt """ # TODO: document _truncate_salt() # XXX: allow providing raw salt to this class, and encoding it? #=================================================================== # class attrs #=================================================================== min_salt_size = None max_salt_size = None salt_chars = None @classproperty def default_salt_size(cls): """default salt size (defaults to *max_salt_size*)""" return cls.max_salt_size @classproperty def default_salt_chars(cls): """charset used to generate new salt strings (defaults to *salt_chars*)""" return cls.salt_chars # private helpers for HasRawSalt, shouldn't be used by subclasses _salt_is_bytes = False _salt_unit = "chars" #=================================================================== # instance attrs #=================================================================== salt = None #=================================================================== # 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) 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 salt_size: optionally specified size of autogenerated salt :raises TypeError: If salt not provided and ``use_defaults=False``. :raises ValueError: * if salt contains chars that aren't in :attr:`salt_chars`. * if salt contains less than :attr:`min_salt_size` characters. * 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 if salt is None: 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 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 = self.min_salt_size if mn and len(salt) < mn: 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 @staticmethod def _truncate_salt(salt, mx): # NOTE: some hashes (e.g. bcrypt) has structure within their # salt string. this provides a method to override 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 A variant of :class:`!HasSalt` which takes in decoded bytes instead of an encoded string. .. todo:: document this class's usage """ 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. _salt_is_bytes = True _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. :param rounds: optional number of rounds hash should use Class Attributes ================ In order for :meth:`!_norm_rounds` to do its job, the following attributes must be provided by the handler subclass: .. attribute:: min_rounds 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 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 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 [required] The ``rounds`` parameter typically encodes a cpu-time cost for calculating a hash. This should be set to ``"linear"`` (the default) or ``"log2"``, depending on how the rounds value relates to the actual amount of time that will be required. 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`) Subclassable Methods ==================== .. automethod:: _norm_rounds """ #=================================================================== # class attrs #=================================================================== min_rounds = 0 max_rounds = None default_rounds = None rounds_cost = "linear" # default to the common case #=================================================================== # instance attrs #=================================================================== rounds = None #=================================================================== # init #=================================================================== def __init__(self, rounds=None, **kwds): super(HasRounds, self).__init__(**kwds) self.rounds = self._norm_rounds(rounds) def _norm_rounds(self, rounds): """helper routine for normalizing rounds :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 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 """ # fill in default if rounds is None: if not self.use_defaults: raise TypeError("no rounds specified") rounds = self.default_rounds if rounds is None: raise TypeError("%s rounds value must be specified explicitly" % (self.name,)) # check type if not isinstance(rounds, int_types): raise exc.ExpectedTypeError(rounds, "integer", "rounds") # check bounds mn = self.min_rounds if 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 = self.max_rounds if mx and 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 @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. .. todo:: finish documenting this class's usage 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, and will dynamically chose the best one at runtime. Backend Methods --------------- .. automethod:: get_backend .. automethod:: set_backend .. automethod:: has_backend Subclass Hooks -------------- The following attributes and methods should be filled in by the subclass 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). .. attribute:: _has_backend_{name} private class attribute checked by :meth:`has_backend` to see if a 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` 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 backends = None # list of backend names, provided by subclass. _backend = None # holds currently loaded backend (if any) or None @classmethod def get_backend(cls): """return name of currently active backend. if no backend has been loaded, loads and returns name of default backend. :raises passlib.exc.MissingBackendError: if no backends are available. :returns: name of active backend """ name = cls._backend if not name: cls.set_backend() name = cls._backend assert name, "set_backend() didn't load any backends" return name @classmethod def has_backend(cls, name="any"): """check if support is currently available for specified backend. :arg name: name of backend to check for. defaults to ``"any"``, but can be any string accepted by :meth:`set_backend`. :raises ValueError: if backend name is unknown :returns: ``True`` if backend is currently supported, else ``False``. """ if name in ("any", "default"): if name == "any" and cls._backend: return True return any(getattr(cls, "_has_backend_" + name) for name in cls.backends) elif name in cls.backends: return getattr(cls, "_has_backend_" + name) else: raise ValueError("unknown backend: %r" % (name,)) @classmethod def _no_backends_msg(cls): return "no %s backends available" % (cls.name,) @classmethod def set_backend(cls, name="any"): """load specified backend to be used for future _calc_checksum() calls this method replaces :meth:`_calc_checksum` with a method which uses the specified backend. :arg name: name of backend to load, defaults to ``"any"``. this can be any of the following values: * any string in :attr:`backends`, indicating the specific backend to use. * the special string ``"default"``, which means to use the preferred backend on the given host (this is generally the first backend in :attr:`backends` which can be loaded). * the special string ``"any"``, which means to use the current backend if one has been loaded, else acts like ``"default"``. :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. :returns: The return value of this function should be ignored. """ if name == "any": name = cls._backend if name: return name name = "default" if name == "default": for name in cls.backends: if cls.has_backend(name): break else: raise exc.MissingBackendError(cls._no_backends_msg()) elif not cls.has_backend(name): raise exc.MissingBackendError("%s backend not available: %r" % (cls.name, name)) cls._calc_checksum_backend = getattr(cls, "_calc_checksum_" + name) cls._backend = name return name def _calc_checksum_backend(self, secret): """ stub for _calc_checksum_backend(), the default backend will be selected the 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" # this should now invoke the backend-specific version, so call it again. return self._calc_checksum_backend(secret) def _calc_checksum(self, secret): """wrapper for backend, for common code""" return self._calc_checksum_backend(secret) #============================================================================= # wrappers #============================================================================= class PrefixWrapper(object): """wraps another handler, adding a constant prefix. instances of this class wrap another password hash handler, altering the constant prefix that's prepended to the wrapped handlers' hashes. this is used mainly by the :doc:`ldap crypt ` handlers; such as :class:`~passlib.hash.ldap_md5_crypt` which wraps :class:`~passlib.hash.md5_crypt` and adds a ``{CRYPT}`` prefix. usage:: myhandler = PrefixWrapper("myhandler", "md5_crypt", prefix="$mh$", orig_prefix="$1$") :param name: name to assign to handler :param wrapped: handler object or name of registered handler :param prefix: identifying prefix to prepend to all hashes :param orig_prefix: prefix to strip (defaults to ''). :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, ident=None): self.name = name if isinstance(prefix, bytes): prefix = prefix.decode("ascii") self.prefix = prefix if isinstance(orig_prefix, bytes): orig_prefix = orig_prefix.decode("ascii") self.orig_prefix = orig_prefix if doc: self.__doc__ = doc if hasattr(wrapped, "name"): self._check_handler(wrapped) self._wrapped_handler = wrapped else: self._wrapped_name = wrapped 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,), exc.PasslibRuntimeWarning) def _get_wrapped(self): handler = self._wrapped_handler if handler is None: handler = get_crypt_handler(self._wrapped_name) self._check_handler(handler) self._wrapped_handler = handler return handler wrapped = property(_get_wrapped) _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 _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", ) def __repr__(self): args = [ repr(self._wrapped_name or self._wrapped_handler) ] if self.prefix: args.append("prefix=%r" % self.prefix) if 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 (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""" # NOTE: assumes hash has been validated as unicode already prefix = self.prefix if not hash.startswith(prefix): 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. # (which does mean extra work under py2, but not py3) if isinstance(hash, bytes): hash = hash.decode("ascii") orig_prefix = self.orig_prefix if not hash.startswith(orig_prefix): raise exc.InvalidHashError(self.wrapped) wrapped = self.prefix + hash[len(orig_prefix):] return uascii_to_str(wrapped) def identify(self, hash): hash = to_unicode_for_identify(hash) if not hash.startswith(self.prefix): return False hash = self._unwrap_hash(hash) return self.wrapped.identify(hash) def genconfig(self, **kwds): config = self.wrapped.genconfig(**kwds) if config is None: return None else: return self._wrap_hash(config) def genhash(self, secret, config, **kwds): 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)) def encrypt(self, secret, **kwds): return self._wrap_hash(self.wrapped.encrypt(secret, **kwds)) def verify(self, secret, hash, **kwds): hash = to_unicode(hash, "ascii", "hash") hash = self._unwrap_hash(hash) return self.wrapped.verify(secret, hash, **kwds) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/utils/compat.py0000644000175000017500000003157012555044153020752 0ustar biscuitbiscuit00000000000000"""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) native_string_types = (unicode,) 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 native_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 #============================================================================= passlib-1.6.5/passlib/utils/pbkdf2.py0000644000175000017500000003454112555044153020640 0ustar biscuitbiscuit00000000000000"""passlib.pbkdf2 - PBKDF2 support this module is getting increasingly poorly named. maybe rename to "kdf" since it's getting more key derivation functions added. """ #============================================================================= # imports #============================================================================= # core import hashlib import logging; log = logging.getLogger(__name__) import re from struct import pack from warnings import warn # site try: from M2Crypto import EVP as _EVP except ImportError: _EVP = None # 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__ = [ "get_prf", "pbkdf1", "pbkdf2", ] #============================================================================= # 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. 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: 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)) def _get_hmac_prf(digest): """helper to return HMAC prf for specific digest""" 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 its 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 hmac_const = _EVP.hmac def prf(key, msg): return hmac_const(key, msg, digest) digest_size = len(result) tag_wrapper(prf) return prf, digest_size # fall back to hashlib-based implementation digest_const = getattr(hashlib, digest, None) if not digest_const: raise ValueError("unknown hash algorithm: %r" % (digest,)) 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): # 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) _prf_cache = {} def _clear_prf_cache(): """helper for unit tests""" _prf_cache.clear() def get_prf(name): """lookup pseudo-random family (prf) by name. :arg name: this must be the name of a recognized prf. currently this only recognizes names with the format :samp:`hmac-{digest}`, where :samp:`{digest}` is the name of a hash function such as ``md5``, ``sha256``, etc. this can also be a callable with the signature ``prf(secret, message) -> digest``, in which case it will be returned unchanged. :raises ValueError: if the name is not known :raises TypeError: if the name is not a callable or string :returns: a tuple of :samp:`({func}, {digest_size})`. * :samp:`{func}` is a function implementing the specified prf, and has the signature ``func(secret, message) -> digest``. * :samp:`{digest_size}` is an integer indicating the number of bytes the function returns. usage example:: >>> from passlib.utils.pbkdf2 import get_prf >>> hmac_sha256, dsize = get_prf("hmac-sha256") >>> hmac_sha256 >>> dsize 32 >>> digest = hmac_sha256('password', 'message') this function will attempt to return the fastest implementation it can find; if M2Crypto is present, and supports the specified prf, :func:`M2Crypto.EVP.hmac` will be used behind the scenes. """ global _prf_cache if name in _prf_cache: return _prf_cache[name] 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 digest_size = len(name(b('x'),b('y'))) retval = (name, digest_size) else: raise ExpectedTypeError(name, "str or callable", "prf name") _prf_cache[name] = retval return retval #============================================================================= # 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 (if ``None``, uses digest's native size) :param hash: hash function to use. must be name of a hash recognized by hashlib. :returns: raw bytes of generated key .. note:: 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. """ # validate secret & salt if not isinstance(secret, bytes): raise ExpectedTypeError(secret, "bytes", "secret") if not isinstance(salt, bytes): raise ExpectedTypeError(salt, "bytes", "salt") # validate rounds if not isinstance(rounds, int_types): raise ExpectedTypeError(rounds, "int", "rounds") if rounds < 1: raise ValueError("rounds must be at least 1") # 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") 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 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=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. 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 the PBKDF2 specification) :returns: raw bytes of generated key """ # validate secret & salt if not isinstance(secret, bytes): raise ExpectedTypeError(secret, "bytes", "secret") if not isinstance(salt, bytes): raise ExpectedTypeError(salt, "bytes", "salt") # validate rounds if not isinstance(rounds, int_types): raise ExpectedTypeError(rounds, "int", "rounds") if rounds < 1: raise ValueError("rounds must be at least 1") # 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: 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 for digest") # 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 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 #============================================================================= passlib-1.6.5/passlib/utils/des.py0000644000175000017500000014604712555044153020250 0ustar biscuitbiscuit00000000000000"""passlib.utils.des -- DES block encryption routines History ======= These routines (which have since been drastically modified for python) are based on a Java implementation of the des-crypt algorithm, found at ``_. The copyright & license for that source is as follows:: UnixCrypt.java 0.9 96/11/25 Copyright (c) 1996 Aki Yoshida. All rights reserved. Permission to use, copy, modify and distribute this software for non-commercial or commercial purposes and without fee is hereby granted provided that this copyright notice appears in all copies. --- Unix crypt(3C) utility @version 0.9, 11/25/96 @author Aki Yoshida --- modified April 2001 by Iris Van den Broeke, Daniel Deville --- Unix Crypt. Implements the one way cryptography used by Unix systems for simple password protection. @version $Id: UnixCrypt2.txt,v 1.1.1.1 2005/09/13 22:20:13 christos Exp $ @author Greg Wilkins (gregw) 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 #============================================================================= # 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", ] #============================================================================= # 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(): """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 PC1ROT=( ( 0x0000000000000000, 0x0000000000000000, 0x0000000000002000, 0x0000000000002000, 0x0000000000000020, 0x0000000000000020, 0x0000000000002020, 0x0000000000002020, 0x0000000000000400, 0x0000000000000400, 0x0000000000002400, 0x0000000000002400, 0x0000000000000420, 0x0000000000000420, 0x0000000000002420, 0x0000000000002420, ), ( 0x0000000000000000, 0x2000000000000000, 0x0000000400000000, 0x2000000400000000, 0x0000800000000000, 0x2000800000000000, 0x0000800400000000, 0x2000800400000000, 0x0008000000000000, 0x2008000000000000, 0x0008000400000000, 0x2008000400000000, 0x0008800000000000, 0x2008800000000000, 0x0008800400000000, 0x2008800400000000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000040, 0x0000000000000040, 0x0000000020000000, 0x0000000020000000, 0x0000000020000040, 0x0000000020000040, 0x0000000000200000, 0x0000000000200000, 0x0000000000200040, 0x0000000000200040, 0x0000000020200000, 0x0000000020200000, 0x0000000020200040, 0x0000000020200040, ), ( 0x0000000000000000, 0x0002000000000000, 0x0800000000000000, 0x0802000000000000, 0x0100000000000000, 0x0102000000000000, 0x0900000000000000, 0x0902000000000000, 0x4000000000000000, 0x4002000000000000, 0x4800000000000000, 0x4802000000000000, 0x4100000000000000, 0x4102000000000000, 0x4900000000000000, 0x4902000000000000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000040000, 0x0000000000040000, 0x0000020000000000, 0x0000020000000000, 0x0000020000040000, 0x0000020000040000, 0x0000000000000004, 0x0000000000000004, 0x0000000000040004, 0x0000000000040004, 0x0000020000000004, 0x0000020000000004, 0x0000020000040004, 0x0000020000040004, ), ( 0x0000000000000000, 0x0000400000000000, 0x0200000000000000, 0x0200400000000000, 0x0080000000000000, 0x0080400000000000, 0x0280000000000000, 0x0280400000000000, 0x0000008000000000, 0x0000408000000000, 0x0200008000000000, 0x0200408000000000, 0x0080008000000000, 0x0080408000000000, 0x0280008000000000, 0x0280408000000000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000010000000, 0x0000000010000000, 0x0000000000001000, 0x0000000000001000, 0x0000000010001000, 0x0000000010001000, 0x0000000040000000, 0x0000000040000000, 0x0000000050000000, 0x0000000050000000, 0x0000000040001000, 0x0000000040001000, 0x0000000050001000, 0x0000000050001000, ), ( 0x0000000000000000, 0x0000001000000000, 0x0000080000000000, 0x0000081000000000, 0x1000000000000000, 0x1000001000000000, 0x1000080000000000, 0x1000081000000000, 0x0004000000000000, 0x0004001000000000, 0x0004080000000000, 0x0004081000000000, 0x1004000000000000, 0x1004001000000000, 0x1004080000000000, 0x1004081000000000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000080, 0x0000000000000080, 0x0000000000080000, 0x0000000000080000, 0x0000000000080080, 0x0000000000080080, 0x0000000000800000, 0x0000000000800000, 0x0000000000800080, 0x0000000000800080, 0x0000000000880000, 0x0000000000880000, 0x0000000000880080, 0x0000000000880080, ), ( 0x0000000000000000, 0x0000000008000000, 0x0000002000000000, 0x0000002008000000, 0x0000100000000000, 0x0000100008000000, 0x0000102000000000, 0x0000102008000000, 0x0000200000000000, 0x0000200008000000, 0x0000202000000000, 0x0000202008000000, 0x0000300000000000, 0x0000300008000000, 0x0000302000000000, 0x0000302008000000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000400000, 0x0000000000400000, 0x0000000004000000, 0x0000000004000000, 0x0000000004400000, 0x0000000004400000, 0x0000000000000800, 0x0000000000000800, 0x0000000000400800, 0x0000000000400800, 0x0000000004000800, 0x0000000004000800, 0x0000000004400800, 0x0000000004400800, ), ( 0x0000000000000000, 0x0000000000008000, 0x0040000000000000, 0x0040000000008000, 0x0000004000000000, 0x0000004000008000, 0x0040004000000000, 0x0040004000008000, 0x8000000000000000, 0x8000000000008000, 0x8040000000000000, 0x8040000000008000, 0x8000004000000000, 0x8000004000008000, 0x8040004000000000, 0x8040004000008000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000004000, 0x0000000000004000, 0x0000000000000008, 0x0000000000000008, 0x0000000000004008, 0x0000000000004008, 0x0000000000000010, 0x0000000000000010, 0x0000000000004010, 0x0000000000004010, 0x0000000000000018, 0x0000000000000018, 0x0000000000004018, 0x0000000000004018, ), ( 0x0000000000000000, 0x0000000200000000, 0x0001000000000000, 0x0001000200000000, 0x0400000000000000, 0x0400000200000000, 0x0401000000000000, 0x0401000200000000, 0x0020000000000000, 0x0020000200000000, 0x0021000000000000, 0x0021000200000000, 0x0420000000000000, 0x0420000200000000, 0x0421000000000000, 0x0421000200000000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000010000000000, 0x0000010000000000, 0x0000000100000000, 0x0000000100000000, 0x0000010100000000, 0x0000010100000000, 0x0000000000100000, 0x0000000000100000, 0x0000010000100000, 0x0000010000100000, 0x0000000100100000, 0x0000000100100000, 0x0000010100100000, 0x0000010100100000, ), ( 0x0000000000000000, 0x0000000080000000, 0x0000040000000000, 0x0000040080000000, 0x0010000000000000, 0x0010000080000000, 0x0010040000000000, 0x0010040080000000, 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 PC2ROTA=( ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000200000, 0x0000000000200000, 0x0000000000200000, 0x0000000000200000, 0x0000000004000000, 0x0000000004000000, 0x0000000004000000, 0x0000000004000000, 0x0000000004200000, 0x0000000004200000, 0x0000000004200000, 0x0000000004200000, ), ( 0x0000000000000000, 0x0000000000000800, 0x0000010000000000, 0x0000010000000800, 0x0000000000002000, 0x0000000000002800, 0x0000010000002000, 0x0000010000002800, 0x0000000010000000, 0x0000000010000800, 0x0000010010000000, 0x0000010010000800, 0x0000000010002000, 0x0000000010002800, 0x0000010010002000, 0x0000010010002800, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000100000000, 0x0000000100000000, 0x0000000100000000, 0x0000000100000000, 0x0000000000800000, 0x0000000000800000, 0x0000000000800000, 0x0000000000800000, 0x0000000100800000, 0x0000000100800000, 0x0000000100800000, 0x0000000100800000, ), ( 0x0000000000000000, 0x0000020000000000, 0x0000000080000000, 0x0000020080000000, 0x0000000000400000, 0x0000020000400000, 0x0000000080400000, 0x0000020080400000, 0x0000000008000000, 0x0000020008000000, 0x0000000088000000, 0x0000020088000000, 0x0000000008400000, 0x0000020008400000, 0x0000000088400000, 0x0000020088400000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000040, 0x0000000000000040, 0x0000000000000040, 0x0000000000000040, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001040, 0x0000000000001040, 0x0000000000001040, 0x0000000000001040, ), ( 0x0000000000000000, 0x0000000000000010, 0x0000000000000400, 0x0000000000000410, 0x0000000000000080, 0x0000000000000090, 0x0000000000000480, 0x0000000000000490, 0x0000000040000000, 0x0000000040000010, 0x0000000040000400, 0x0000000040000410, 0x0000000040000080, 0x0000000040000090, 0x0000000040000480, 0x0000000040000490, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000100000, 0x0000000000100000, 0x0000000000100000, 0x0000000000100000, 0x0000000000180000, 0x0000000000180000, 0x0000000000180000, 0x0000000000180000, ), ( 0x0000000000000000, 0x0000000000040000, 0x0000000000000020, 0x0000000000040020, 0x0000000000000004, 0x0000000000040004, 0x0000000000000024, 0x0000000000040024, 0x0000000200000000, 0x0000000200040000, 0x0000000200000020, 0x0000000200040020, 0x0000000200000004, 0x0000000200040004, 0x0000000200000024, 0x0000000200040024, ), ( 0x0000000000000000, 0x0000000000000008, 0x0000000000008000, 0x0000000000008008, 0x0010000000000000, 0x0010000000000008, 0x0010000000008000, 0x0010000000008008, 0x0020000000000000, 0x0020000000000008, 0x0020000000008000, 0x0020000000008008, 0x0030000000000000, 0x0030000000000008, 0x0030000000008000, 0x0030000000008008, ), ( 0x0000000000000000, 0x0000400000000000, 0x0000080000000000, 0x0000480000000000, 0x0000100000000000, 0x0000500000000000, 0x0000180000000000, 0x0000580000000000, 0x4000000000000000, 0x4000400000000000, 0x4000080000000000, 0x4000480000000000, 0x4000100000000000, 0x4000500000000000, 0x4000180000000000, 0x4000580000000000, ), ( 0x0000000000000000, 0x0000000000004000, 0x0000000020000000, 0x0000000020004000, 0x0001000000000000, 0x0001000000004000, 0x0001000020000000, 0x0001000020004000, 0x0200000000000000, 0x0200000000004000, 0x0200000020000000, 0x0200000020004000, 0x0201000000000000, 0x0201000000004000, 0x0201000020000000, 0x0201000020004000, ), ( 0x0000000000000000, 0x1000000000000000, 0x0004000000000000, 0x1004000000000000, 0x0002000000000000, 0x1002000000000000, 0x0006000000000000, 0x1006000000000000, 0x0000000800000000, 0x1000000800000000, 0x0004000800000000, 0x1004000800000000, 0x0002000800000000, 0x1002000800000000, 0x0006000800000000, 0x1006000800000000, ), ( 0x0000000000000000, 0x0040000000000000, 0x2000000000000000, 0x2040000000000000, 0x0000008000000000, 0x0040008000000000, 0x2000008000000000, 0x2040008000000000, 0x0000001000000000, 0x0040001000000000, 0x2000001000000000, 0x2040001000000000, 0x0000009000000000, 0x0040009000000000, 0x2000009000000000, 0x2040009000000000, ), ( 0x0000000000000000, 0x0400000000000000, 0x8000000000000000, 0x8400000000000000, 0x0000002000000000, 0x0400002000000000, 0x8000002000000000, 0x8400002000000000, 0x0100000000000000, 0x0500000000000000, 0x8100000000000000, 0x8500000000000000, 0x0100002000000000, 0x0500002000000000, 0x8100002000000000, 0x8500002000000000, ), ( 0x0000000000000000, 0x0000800000000000, 0x0800000000000000, 0x0800800000000000, 0x0000004000000000, 0x0000804000000000, 0x0800004000000000, 0x0800804000000000, 0x0000000400000000, 0x0000800400000000, 0x0800000400000000, 0x0800800400000000, 0x0000004400000000, 0x0000804400000000, 0x0800004400000000, 0x0800804400000000, ), ( 0x0000000000000000, 0x0080000000000000, 0x0000040000000000, 0x0080040000000000, 0x0008000000000000, 0x0088000000000000, 0x0008040000000000, 0x0088040000000000, 0x0000200000000000, 0x0080200000000000, 0x0000240000000000, 0x0080240000000000, 0x0008200000000000, 0x0088200000000000, 0x0008240000000000, 0x0088240000000000, ), ) # NOTE: this was reordered from original table to make perm3264 logic simpler PC2ROTB=( ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000400, 0x0000000000000400, 0x0000000000000400, 0x0000000000000400, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080000, 0x0000000000080400, 0x0000000000080400, 0x0000000000080400, 0x0000000000080400, ), ( 0x0000000000000000, 0x0000000000800000, 0x0000000000004000, 0x0000000000804000, 0x0000000080000000, 0x0000000080800000, 0x0000000080004000, 0x0000000080804000, 0x0000000000040000, 0x0000000000840000, 0x0000000000044000, 0x0000000000844000, 0x0000000080040000, 0x0000000080840000, 0x0000000080044000, 0x0000000080844000, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000008, 0x0000000000000008, 0x0000000000000008, 0x0000000000000008, 0x0000000040000000, 0x0000000040000000, 0x0000000040000000, 0x0000000040000000, 0x0000000040000008, 0x0000000040000008, 0x0000000040000008, 0x0000000040000008, ), ( 0x0000000000000000, 0x0000000020000000, 0x0000000200000000, 0x0000000220000000, 0x0000000000000080, 0x0000000020000080, 0x0000000200000080, 0x0000000220000080, 0x0000000000100000, 0x0000000020100000, 0x0000000200100000, 0x0000000220100000, 0x0000000000100080, 0x0000000020100080, 0x0000000200100080, 0x0000000220100080, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000002000, 0x0000000000002000, 0x0000000000002000, 0x0000000000002000, 0x0000020000000000, 0x0000020000000000, 0x0000020000000000, 0x0000020000000000, 0x0000020000002000, 0x0000020000002000, 0x0000020000002000, 0x0000020000002000, ), ( 0x0000000000000000, 0x0000000000000800, 0x0000000100000000, 0x0000000100000800, 0x0000000010000000, 0x0000000010000800, 0x0000000110000000, 0x0000000110000800, 0x0000000000000004, 0x0000000000000804, 0x0000000100000004, 0x0000000100000804, 0x0000000010000004, 0x0000000010000804, 0x0000000110000004, 0x0000000110000804, ), ( 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000001000, 0x0000000000000010, 0x0000000000000010, 0x0000000000000010, 0x0000000000000010, 0x0000000000001010, 0x0000000000001010, 0x0000000000001010, 0x0000000000001010, ), ( 0x0000000000000000, 0x0000000000000040, 0x0000010000000000, 0x0000010000000040, 0x0000000000200000, 0x0000000000200040, 0x0000010000200000, 0x0000010000200040, 0x0000000000008000, 0x0000000000008040, 0x0000010000008000, 0x0000010000008040, 0x0000000000208000, 0x0000000000208040, 0x0000010000208000, 0x0000010000208040, ), ( 0x0000000000000000, 0x0000000004000000, 0x0000000008000000, 0x000000000c000000, 0x0400000000000000, 0x0400000004000000, 0x0400000008000000, 0x040000000c000000, 0x8000000000000000, 0x8000000004000000, 0x8000000008000000, 0x800000000c000000, 0x8400000000000000, 0x8400000004000000, 0x8400000008000000, 0x840000000c000000, ), ( 0x0000000000000000, 0x0002000000000000, 0x0200000000000000, 0x0202000000000000, 0x1000000000000000, 0x1002000000000000, 0x1200000000000000, 0x1202000000000000, 0x0008000000000000, 0x000a000000000000, 0x0208000000000000, 0x020a000000000000, 0x1008000000000000, 0x100a000000000000, 0x1208000000000000, 0x120a000000000000, ), ( 0x0000000000000000, 0x0000000000400000, 0x0000000000000020, 0x0000000000400020, 0x0040000000000000, 0x0040000000400000, 0x0040000000000020, 0x0040000000400020, 0x0800000000000000, 0x0800000000400000, 0x0800000000000020, 0x0800000000400020, 0x0840000000000000, 0x0840000000400000, 0x0840000000000020, 0x0840000000400020, ), ( 0x0000000000000000, 0x0080000000000000, 0x0000008000000000, 0x0080008000000000, 0x2000000000000000, 0x2080000000000000, 0x2000008000000000, 0x2080008000000000, 0x0020000000000000, 0x00a0000000000000, 0x0020008000000000, 0x00a0008000000000, 0x2020000000000000, 0x20a0000000000000, 0x2020008000000000, 0x20a0008000000000, ), ( 0x0000000000000000, 0x0000002000000000, 0x0000040000000000, 0x0000042000000000, 0x4000000000000000, 0x4000002000000000, 0x4000040000000000, 0x4000042000000000, 0x0000400000000000, 0x0000402000000000, 0x0000440000000000, 0x0000442000000000, 0x4000400000000000, 0x4000402000000000, 0x4000440000000000, 0x4000442000000000, ), ( 0x0000000000000000, 0x0000004000000000, 0x0000200000000000, 0x0000204000000000, 0x0000080000000000, 0x0000084000000000, 0x0000280000000000, 0x0000284000000000, 0x0000800000000000, 0x0000804000000000, 0x0000a00000000000, 0x0000a04000000000, 0x0000880000000000, 0x0000884000000000, 0x0000a80000000000, 0x0000a84000000000, ), ( 0x0000000000000000, 0x0000000800000000, 0x0000000400000000, 0x0000000c00000000, 0x0000100000000000, 0x0000100800000000, 0x0000100400000000, 0x0000100c00000000, 0x0010000000000000, 0x0010000800000000, 0x0010000400000000, 0x0010000c00000000, 0x0010100000000000, 0x0010100800000000, 0x0010100400000000, 0x0010100c00000000, ), ( 0x0000000000000000, 0x0100000000000000, 0x0001000000000000, 0x0101000000000000, 0x0000001000000000, 0x0100001000000000, 0x0001001000000000, 0x0101001000000000, 0x0004000000000000, 0x0104000000000000, 0x0005000000000000, 0x0105000000000000, 0x0004001000000000, 0x0104001000000000, 0x0005001000000000, 0x0105001000000000, ), ) #--------------------------------------------------------------- # 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, ## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTB, ## PC2ROTA, PC2ROTB, PC2ROTB, PC2ROTB, ## PC2ROTB, PC2ROTB, PC2ROTB, PC2ROTA, ## ) # 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), (PC2ROTA, PC2ROTB), (PC2ROTB, PC2ROTB), (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 IE3264=( ( 0x0000000000000000, 0x0000000000800800, 0x0000000000008008, 0x0000000000808808, 0x0000008008000000, 0x0000008008800800, 0x0000008008008008, 0x0000008008808808, 0x0000000080080000, 0x0000000080880800, 0x0000000080088008, 0x0000000080888808, 0x0000008088080000, 0x0000008088880800, 0x0000008088088008, 0x0000008088888808, ), ( 0x0000000000000000, 0x0080080000000000, 0x0000800800000000, 0x0080880800000000, 0x0800000000000080, 0x0880080000000080, 0x0800800800000080, 0x0880880800000080, 0x8008000000000000, 0x8088080000000000, 0x8008800800000000, 0x8088880800000000, 0x8808000000000080, 0x8888080000000080, 0x8808800800000080, 0x8888880800000080, ), ( 0x0000000000000000, 0x0000000000001000, 0x0000000000000010, 0x0000000000001010, 0x0000000010000000, 0x0000000010001000, 0x0000000010000010, 0x0000000010001010, 0x0000000000100000, 0x0000000000101000, 0x0000000000100010, 0x0000000000101010, 0x0000000010100000, 0x0000000010101000, 0x0000000010100010, 0x0000000010101010, ), ( 0x0000000000000000, 0x0000100000000000, 0x0000001000000000, 0x0000101000000000, 0x1000000000000000, 0x1000100000000000, 0x1000001000000000, 0x1000101000000000, 0x0010000000000000, 0x0010100000000000, 0x0010001000000000, 0x0010101000000000, 0x1010000000000000, 0x1010100000000000, 0x1010001000000000, 0x1010101000000000, ), ( 0x0000000000000000, 0x0000000000002000, 0x0000000000000020, 0x0000000000002020, 0x0000000020000000, 0x0000000020002000, 0x0000000020000020, 0x0000000020002020, 0x0000000000200000, 0x0000000000202000, 0x0000000000200020, 0x0000000000202020, 0x0000000020200000, 0x0000000020202000, 0x0000000020200020, 0x0000000020202020, ), ( 0x0000000000000000, 0x0000200000000000, 0x0000002000000000, 0x0000202000000000, 0x2000000000000000, 0x2000200000000000, 0x2000002000000000, 0x2000202000000000, 0x0020000000000000, 0x0020200000000000, 0x0020002000000000, 0x0020202000000000, 0x2020000000000000, 0x2020200000000000, 0x2020002000000000, 0x2020202000000000, ), ( 0x0000000000000000, 0x0000000000004004, 0x0400000000000040, 0x0400000000004044, 0x0000000040040000, 0x0000000040044004, 0x0400000040040040, 0x0400000040044044, 0x0000000000400400, 0x0000000000404404, 0x0400000000400440, 0x0400000000404444, 0x0000000040440400, 0x0000000040444404, 0x0400000040440440, 0x0400000040444444, ), ( 0x0000000000000000, 0x0000400400000000, 0x0000004004000000, 0x0000404404000000, 0x4004000000000000, 0x4004400400000000, 0x4004004004000000, 0x4004404404000000, 0x0040040000000000, 0x0040440400000000, 0x0040044004000000, 0x0040444404000000, 0x4044040000000000, 0x4044440400000000, 0x4044044004000000, 0x4044444404000000, ), ) #--------------------------------------------------------------- # Table that combines the S, P, and E operations. #--------------------------------------------------------------- SPE=( ( 0x0080088008200000, 0x0000008008000000, 0x0000000000200020, 0x0080088008200020, 0x0000000000200000, 0x0080088008000020, 0x0000008008000020, 0x0000000000200020, 0x0080088008000020, 0x0080088008200000, 0x0000008008200000, 0x0080080000000020, 0x0080080000200020, 0x0000000000200000, 0x0000000000000000, 0x0000008008000020, 0x0000008008000000, 0x0000000000000020, 0x0080080000200000, 0x0080088008000000, 0x0080088008200020, 0x0000008008200000, 0x0080080000000020, 0x0080080000200000, 0x0000000000000020, 0x0080080000000000, 0x0080088008000000, 0x0000008008200020, 0x0080080000000000, 0x0080080000200020, 0x0000008008200020, 0x0000000000000000, 0x0000000000000000, 0x0080088008200020, 0x0080080000200000, 0x0000008008000020, 0x0080088008200000, 0x0000008008000000, 0x0080080000000020, 0x0080080000200000, 0x0000008008200020, 0x0080080000000000, 0x0080088008000000, 0x0000000000200020, 0x0080088008000020, 0x0000000000000020, 0x0000000000200020, 0x0000008008200000, 0x0080088008200020, 0x0080088008000000, 0x0000008008200000, 0x0080080000200020, 0x0000000000200000, 0x0080080000000020, 0x0000008008000020, 0x0000000000000000, 0x0000008008000000, 0x0000000000200000, 0x0080080000200020, 0x0080088008200000, 0x0000000000000020, 0x0000008008200020, 0x0080080000000000, 0x0080088008000020, ), ( 0x1000800810004004, 0x0000000000000000, 0x0000800810000000, 0x0000000010004004, 0x1000000000004004, 0x1000800800000000, 0x0000800800004004, 0x0000800810000000, 0x0000800800000000, 0x1000000010004004, 0x1000000000000000, 0x0000800800004004, 0x1000000010000000, 0x0000800810004004, 0x0000000010004004, 0x1000000000000000, 0x0000000010000000, 0x1000800800004004, 0x1000000010004004, 0x0000800800000000, 0x1000800810000000, 0x0000000000004004, 0x0000000000000000, 0x1000000010000000, 0x1000800800004004, 0x1000800810000000, 0x0000800810004004, 0x1000000000004004, 0x0000000000004004, 0x0000000010000000, 0x1000800800000000, 0x1000800810004004, 0x1000000010000000, 0x0000800810004004, 0x0000800800004004, 0x1000800810000000, 0x1000800810004004, 0x1000000010000000, 0x1000000000004004, 0x0000000000000000, 0x0000000000004004, 0x1000800800000000, 0x0000000010000000, 0x1000000010004004, 0x0000800800000000, 0x0000000000004004, 0x1000800810000000, 0x1000800800004004, 0x0000800810004004, 0x0000800800000000, 0x0000000000000000, 0x1000000000004004, 0x1000000000000000, 0x1000800810004004, 0x0000800810000000, 0x0000000010004004, 0x1000000010004004, 0x0000000010000000, 0x1000800800000000, 0x0000800800004004, 0x1000800800004004, 0x1000000000000000, 0x0000000010004004, 0x0000800810000000, ), ( 0x0000000000400410, 0x0010004004400400, 0x0010000000000000, 0x0010000000400410, 0x0000004004000010, 0x0000000000400400, 0x0010000000400410, 0x0010004004000000, 0x0010000000400400, 0x0000004004000000, 0x0000004004400400, 0x0000000000000010, 0x0010004004400410, 0x0010000000000010, 0x0000000000000010, 0x0000004004400410, 0x0000000000000000, 0x0000004004000010, 0x0010004004400400, 0x0010000000000000, 0x0010000000000010, 0x0010004004400410, 0x0000004004000000, 0x0000000000400410, 0x0000004004400410, 0x0010000000400400, 0x0010004004000010, 0x0000004004400400, 0x0010004004000000, 0x0000000000000000, 0x0000000000400400, 0x0010004004000010, 0x0010004004400400, 0x0010000000000000, 0x0000000000000010, 0x0000004004000000, 0x0010000000000010, 0x0000004004000010, 0x0000004004400400, 0x0010000000400410, 0x0000000000000000, 0x0010004004400400, 0x0010004004000000, 0x0000004004400410, 0x0000004004000010, 0x0000000000400400, 0x0010004004400410, 0x0000000000000010, 0x0010004004000010, 0x0000000000400410, 0x0000000000400400, 0x0010004004400410, 0x0000004004000000, 0x0010000000400400, 0x0010000000400410, 0x0010004004000000, 0x0010000000400400, 0x0000000000000000, 0x0000004004400410, 0x0010000000000010, 0x0000000000400410, 0x0010004004000010, 0x0010000000000000, 0x0000004004400400, ), ( 0x0800100040040080, 0x0000100000001000, 0x0800000000000080, 0x0800100040041080, 0x0000000000000000, 0x0000000040041000, 0x0800100000001080, 0x0800000040040080, 0x0000100040041000, 0x0800000000001080, 0x0000000000001000, 0x0800100000000080, 0x0800000000001080, 0x0800100040040080, 0x0000000040040000, 0x0000000000001000, 0x0800000040041080, 0x0000100040040000, 0x0000100000000000, 0x0800000000000080, 0x0000100040040000, 0x0800100000001080, 0x0000000040041000, 0x0000100000000000, 0x0800100000000080, 0x0000000000000000, 0x0800000040040080, 0x0000100040041000, 0x0000100000001000, 0x0800000040041080, 0x0800100040041080, 0x0000000040040000, 0x0800000040041080, 0x0800100000000080, 0x0000000040040000, 0x0800000000001080, 0x0000100040040000, 0x0000100000001000, 0x0800000000000080, 0x0000000040041000, 0x0800100000001080, 0x0000000000000000, 0x0000100000000000, 0x0800000040040080, 0x0000000000000000, 0x0800000040041080, 0x0000100040041000, 0x0000100000000000, 0x0000000000001000, 0x0800100040041080, 0x0800100040040080, 0x0000000040040000, 0x0800100040041080, 0x0800000000000080, 0x0000100000001000, 0x0800100040040080, 0x0800000040040080, 0x0000100040040000, 0x0000000040041000, 0x0800100000001080, 0x0800100000000080, 0x0000000000001000, 0x0800000000001080, 0x0000100040041000, ), ( 0x0000000000800800, 0x0000001000000000, 0x0040040000000000, 0x2040041000800800, 0x2000001000800800, 0x0040040000800800, 0x2040041000000000, 0x0000001000800800, 0x0000001000000000, 0x2000000000000000, 0x2000000000800800, 0x0040041000000000, 0x2040040000800800, 0x2000001000800800, 0x0040041000800800, 0x0000000000000000, 0x0040041000000000, 0x0000000000800800, 0x2000001000000000, 0x2040040000000000, 0x0040040000800800, 0x2040041000000000, 0x0000000000000000, 0x2000000000800800, 0x2000000000000000, 0x2040040000800800, 0x2040041000800800, 0x2000001000000000, 0x0000001000800800, 0x0040040000000000, 0x2040040000000000, 0x0040041000800800, 0x0040041000800800, 0x2040040000800800, 0x2000001000000000, 0x0000001000800800, 0x0000001000000000, 0x2000000000000000, 0x2000000000800800, 0x0040040000800800, 0x0000000000800800, 0x0040041000000000, 0x2040041000800800, 0x0000000000000000, 0x2040041000000000, 0x0000000000800800, 0x0040040000000000, 0x2000001000000000, 0x2040040000800800, 0x0040040000000000, 0x0000000000000000, 0x2040041000800800, 0x2000001000800800, 0x0040041000800800, 0x2040040000000000, 0x0000001000000000, 0x0040041000000000, 0x2000001000800800, 0x0040040000800800, 0x2040040000000000, 0x2000000000000000, 0x2040041000000000, 0x0000001000800800, 0x2000000000800800, ), ( 0x4004000000008008, 0x4004000020000000, 0x0000000000000000, 0x0000200020008008, 0x4004000020000000, 0x0000200000000000, 0x4004200000008008, 0x0000000020000000, 0x4004200000000000, 0x4004200020008008, 0x0000200020000000, 0x0000000000008008, 0x0000200000008008, 0x4004000000008008, 0x0000000020008008, 0x4004200020000000, 0x0000000020000000, 0x4004200000008008, 0x4004000020008008, 0x0000000000000000, 0x0000200000000000, 0x4004000000000000, 0x0000200020008008, 0x4004000020008008, 0x4004200020008008, 0x0000000020008008, 0x0000000000008008, 0x4004200000000000, 0x4004000000000000, 0x0000200020000000, 0x4004200020000000, 0x0000200000008008, 0x4004200000000000, 0x0000000000008008, 0x0000200000008008, 0x4004200020000000, 0x0000200020008008, 0x4004000020000000, 0x0000000000000000, 0x0000200000008008, 0x0000000000008008, 0x0000200000000000, 0x4004000020008008, 0x0000000020000000, 0x4004000020000000, 0x4004200020008008, 0x0000200020000000, 0x4004000000000000, 0x4004200020008008, 0x0000200020000000, 0x0000000020000000, 0x4004200000008008, 0x4004000000008008, 0x0000000020008008, 0x4004200020000000, 0x0000000000000000, 0x0000200000000000, 0x4004000000008008, 0x4004200000008008, 0x0000200020008008, 0x0000000020008008, 0x4004200000000000, 0x4004000000000000, 0x4004000020008008, ), ( 0x0000400400000000, 0x0020000000000000, 0x0020000000100000, 0x0400000000100040, 0x0420400400100040, 0x0400400400000040, 0x0020400400000000, 0x0000000000000000, 0x0000000000100000, 0x0420000000100040, 0x0420000000000040, 0x0000400400100000, 0x0400000000000040, 0x0020400400100000, 0x0000400400100000, 0x0420000000000040, 0x0420000000100040, 0x0000400400000000, 0x0400400400000040, 0x0420400400100040, 0x0000000000000000, 0x0020000000100000, 0x0400000000100040, 0x0020400400000000, 0x0400400400100040, 0x0420400400000040, 0x0020400400100000, 0x0400000000000040, 0x0420400400000040, 0x0400400400100040, 0x0020000000000000, 0x0000000000100000, 0x0420400400000040, 0x0000400400100000, 0x0400400400100040, 0x0420000000000040, 0x0000400400000000, 0x0020000000000000, 0x0000000000100000, 0x0400400400100040, 0x0420000000100040, 0x0420400400000040, 0x0020400400000000, 0x0000000000000000, 0x0020000000000000, 0x0400000000100040, 0x0400000000000040, 0x0020000000100000, 0x0000000000000000, 0x0420000000100040, 0x0020000000100000, 0x0020400400000000, 0x0420000000000040, 0x0000400400000000, 0x0420400400100040, 0x0000000000100000, 0x0020400400100000, 0x0400000000000040, 0x0400400400000040, 0x0420400400100040, 0x0400000000100040, 0x0020400400100000, 0x0000400400100000, 0x0400400400000040, ), ( 0x8008000080082000, 0x0000002080082000, 0x8008002000000000, 0x0000000000000000, 0x0000002000002000, 0x8008000080080000, 0x0000000080082000, 0x8008002080082000, 0x8008000000000000, 0x0000000000002000, 0x0000002080080000, 0x8008002000000000, 0x8008002080080000, 0x8008002000002000, 0x8008000000002000, 0x0000000080082000, 0x0000002000000000, 0x8008002080080000, 0x8008000080080000, 0x0000002000002000, 0x8008002080082000, 0x8008000000002000, 0x0000000000000000, 0x0000002080080000, 0x0000000000002000, 0x0000000080080000, 0x8008002000002000, 0x8008000080082000, 0x0000000080080000, 0x0000002000000000, 0x0000002080082000, 0x8008000000000000, 0x0000000080080000, 0x0000002000000000, 0x8008000000002000, 0x8008002080082000, 0x8008002000000000, 0x0000000000002000, 0x0000000000000000, 0x0000002080080000, 0x8008000080082000, 0x8008002000002000, 0x0000002000002000, 0x8008000080080000, 0x0000002080082000, 0x8008000000000000, 0x8008000080080000, 0x0000002000002000, 0x8008002080082000, 0x0000000080080000, 0x0000000080082000, 0x8008000000002000, 0x0000002080080000, 0x8008002000000000, 0x8008002000002000, 0x0000000080082000, 0x8008000000000000, 0x0000002080082000, 0x8008002080080000, 0x0000000000000000, 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 CF6464=( ( 0x0000000000000000, 0x0000002000000000, 0x0000200000000000, 0x0000202000000000, 0x0020000000000000, 0x0020002000000000, 0x0020200000000000, 0x0020202000000000, 0x2000000000000000, 0x2000002000000000, 0x2000200000000000, 0x2000202000000000, 0x2020000000000000, 0x2020002000000000, 0x2020200000000000, 0x2020202000000000, ), ( 0x0000000000000000, 0x0000000200000000, 0x0000020000000000, 0x0000020200000000, 0x0002000000000000, 0x0002000200000000, 0x0002020000000000, 0x0002020200000000, 0x0200000000000000, 0x0200000200000000, 0x0200020000000000, 0x0200020200000000, 0x0202000000000000, 0x0202000200000000, 0x0202020000000000, 0x0202020200000000, ), ( 0x0000000000000000, 0x0000000000000020, 0x0000000000002000, 0x0000000000002020, 0x0000000000200000, 0x0000000000200020, 0x0000000000202000, 0x0000000000202020, 0x0000000020000000, 0x0000000020000020, 0x0000000020002000, 0x0000000020002020, 0x0000000020200000, 0x0000000020200020, 0x0000000020202000, 0x0000000020202020, ), ( 0x0000000000000000, 0x0000000000000002, 0x0000000000000200, 0x0000000000000202, 0x0000000000020000, 0x0000000000020002, 0x0000000000020200, 0x0000000000020202, 0x0000000002000000, 0x0000000002000002, 0x0000000002000200, 0x0000000002000202, 0x0000000002020000, 0x0000000002020002, 0x0000000002020200, 0x0000000002020202, ), ( 0x0000000000000000, 0x0000008000000000, 0x0000800000000000, 0x0000808000000000, 0x0080000000000000, 0x0080008000000000, 0x0080800000000000, 0x0080808000000000, 0x8000000000000000, 0x8000008000000000, 0x8000800000000000, 0x8000808000000000, 0x8080000000000000, 0x8080008000000000, 0x8080800000000000, 0x8080808000000000, ), ( 0x0000000000000000, 0x0000000800000000, 0x0000080000000000, 0x0000080800000000, 0x0008000000000000, 0x0008000800000000, 0x0008080000000000, 0x0008080800000000, 0x0800000000000000, 0x0800000800000000, 0x0800080000000000, 0x0800080800000000, 0x0808000000000000, 0x0808000800000000, 0x0808080000000000, 0x0808080800000000, ), ( 0x0000000000000000, 0x0000000000000080, 0x0000000000008000, 0x0000000000008080, 0x0000000000800000, 0x0000000000800080, 0x0000000000808000, 0x0000000000808080, 0x0000000080000000, 0x0000000080000080, 0x0000000080008000, 0x0000000080008080, 0x0000000080800000, 0x0000000080800080, 0x0000000080808000, 0x0000000080808080, ), ( 0x0000000000000000, 0x0000000000000008, 0x0000000000000800, 0x0000000000000808, 0x0000000000080000, 0x0000000000080008, 0x0000000000080800, 0x0000000000080808, 0x0000000008000000, 0x0000000008000008, 0x0000000008000800, 0x0000000008000808, 0x0000000008080000, 0x0000000008080008, 0x0000000008080800, 0x0000000008080808, ), ( 0x0000000000000000, 0x0000001000000000, 0x0000100000000000, 0x0000101000000000, 0x0010000000000000, 0x0010001000000000, 0x0010100000000000, 0x0010101000000000, 0x1000000000000000, 0x1000001000000000, 0x1000100000000000, 0x1000101000000000, 0x1010000000000000, 0x1010001000000000, 0x1010100000000000, 0x1010101000000000, ), ( 0x0000000000000000, 0x0000000100000000, 0x0000010000000000, 0x0000010100000000, 0x0001000000000000, 0x0001000100000000, 0x0001010000000000, 0x0001010100000000, 0x0100000000000000, 0x0100000100000000, 0x0100010000000000, 0x0100010100000000, 0x0101000000000000, 0x0101000100000000, 0x0101010000000000, 0x0101010100000000, ), ( 0x0000000000000000, 0x0000000000000010, 0x0000000000001000, 0x0000000000001010, 0x0000000000100000, 0x0000000000100010, 0x0000000000101000, 0x0000000000101010, 0x0000000010000000, 0x0000000010000010, 0x0000000010001000, 0x0000000010001010, 0x0000000010100000, 0x0000000010100010, 0x0000000010101000, 0x0000000010101010, ), ( 0x0000000000000000, 0x0000000000000001, 0x0000000000000100, 0x0000000000000101, 0x0000000000010000, 0x0000000000010001, 0x0000000000010100, 0x0000000000010101, 0x0000000001000000, 0x0000000001000001, 0x0000000001000100, 0x0000000001000101, 0x0000000001010000, 0x0000000001010001, 0x0000000001010100, 0x0000000001010101, ), ( 0x0000000000000000, 0x0000004000000000, 0x0000400000000000, 0x0000404000000000, 0x0040000000000000, 0x0040004000000000, 0x0040400000000000, 0x0040404000000000, 0x4000000000000000, 0x4000004000000000, 0x4000400000000000, 0x4000404000000000, 0x4040000000000000, 0x4040004000000000, 0x4040400000000000, 0x4040404000000000, ), ( 0x0000000000000000, 0x0000000400000000, 0x0000040000000000, 0x0000040400000000, 0x0004000000000000, 0x0004000400000000, 0x0004040000000000, 0x0004040400000000, 0x0400000000000000, 0x0400000400000000, 0x0400040000000000, 0x0400040400000000, 0x0404000000000000, 0x0404000400000000, 0x0404040000000000, 0x0404040400000000, ), ( 0x0000000000000000, 0x0000000000000040, 0x0000000000004000, 0x0000000000004040, 0x0000000000400000, 0x0000000000400040, 0x0000000000404000, 0x0000000000404040, 0x0000000040000000, 0x0000000040000040, 0x0000000040004000, 0x0000000040004040, 0x0000000040400000, 0x0000000040400040, 0x0000000040404000, 0x0000000040404040, ), ( 0x0000000000000000, 0x0000000000000004, 0x0000000000000400, 0x0000000000000404, 0x0000000000040000, 0x0000000000040004, 0x0000000000040400, 0x0000000000040404, 0x0000000004000000, 0x0000000004000004, 0x0000000004000400, 0x0000000004000404, 0x0000000004040000, 0x0000000004040004, 0x0000000004040400, 0x0000000004040404, ), ) #=================================================================== # eof _load_tables() #=================================================================== #============================================================================= # support #============================================================================= 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. out = 0 for r in p: out |= r[c&0xf] c >>= 4 return out #============================================================================= # packing & unpacking #============================================================================= _uint64_struct = struct.Struct(">Q") _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 _unpack56(value): return _uint64_struct.unpack(_BNULL+value)[0] #============================================================================= # 56->64 key manipulation #============================================================================= ##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 its 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 its 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. """ # 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 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() # load SPE into local vars to speed things up and remove an array access call SPE0, SPE1, SPE2, SPE3, SPE4, SPE5, SPE6, SPE7 = SPE # NOTE: parity bits are ignored completely # (UTs do fuzz testing to ensure this) # 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: 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 per des_crypt & bsdi_crypt salt = ( ((salt & 0x00003f) << 26) | ((salt & 0x000fc0) << 12) | ((salt & 0x03f000) >> 2) | ((salt & 0xfc0000) >> 16) ) # init L & R if input == 0: L = R = 0 else: L = ((input >> 31) & 0xaaaaaaaa) | (input & 0x55555555) L = _permute(L, IE3264) R = ((input >> 32) & 0xaaaaaaaa) | ((input >> 1) & 0x55555555) R = _permute(R, IE3264) #--------------------------------------------------------------- # 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 for ks_even, ks_odd in ks_list: 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] ^ SPE2[(B>>42)&0x3f] ^ SPE3[(B>>34)&0x3f] ^ 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 flip specific bits B = (k<<32) ^ k ^ L ^ ks_odd R ^= (SPE0[(B>>58)&0x3f] ^ SPE1[(B>>50)&0x3f] ^ SPE2[(B>>42)&0x3f] ^ SPE3[(B>>34)&0x3f] ^ SPE4[(B>>26)&0x3f] ^ SPE5[(B>>18)&0x3f] ^ SPE6[(B>>10)&0x3f] ^ SPE7[(B>>2)&0x3f]) # swap L and R L, R = R, L #--------------------------------------------------------------- # return final result #--------------------------------------------------------------- C = ( ((L>>3) & 0x0f0f0f0f00000000) | ((L<<33) & 0xf0f0f0f000000000) | ((R>>35) & 0x000000000f0f0f0f) | ((R<<1) & 0x00000000f0f0f0f0) ) return _permute(C, CF6464) @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 #============================================================================= passlib-1.6.5/passlib/hosts.py0000644000175000017500000000717012555044153017466 0ustar biscuitbiscuit00000000000000"""passlib.hosts""" #============================================================================= # imports #============================================================================= # core import sys from warnings import warn # pkg from passlib.context import LazyCryptContext from passlib.exc import PasslibRuntimeWarning from passlib.registry import get_crypt_handler from passlib.utils import has_crypt, unix_crypt_schemes # local __all__ = [ "linux_context", "linux2_context", "openbsd_context", "netbsd_context", "freebsd_context", "host_context", ] #============================================================================= # linux support #============================================================================= # known platform names - linux2 linux_context = linux2_context = LazyCryptContext( schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt", "des_crypt", "unix_disabled" ], deprecated = [ "des_crypt" ], ) #============================================================================= # bsd support #============================================================================= # known platform names - # freebsd2 # freebsd3 # freebsd4 # freebsd5 # freebsd6 # freebsd7 # # netbsd1 # 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", "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""" found = False for name in unix_crypt_schemes: handler = get_crypt_handler(name) if handler.has_backend("os_crypt"): found = True yield name if found: # 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 #============================================================================= passlib-1.6.5/passlib/exc.py0000644000175000017500000001734412555044153017111 0ustar biscuitbiscuit00000000000000"""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 class PasslibSecurityError(RuntimeError): """ Error raised if critical security issue is detected (e.g. an attempt is made to use a vulnerable version of a bcrypt backend). .. versionadded:: 1.6.3 """ #============================================================================= # warnings #============================================================================= class PasslibWarning(UserWarning): """base class for Passlib's user warnings, derives from the builtin :exc:`UserWarning`. .. 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. .. versionadded:: 1.6 """ 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. .. versionadded:: 1.6 """ 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 anomalous enough that the developers would love to hear under what conditions it occurred. .. versionadded:: 1.6 """ class PasslibSecurityWarning(PasslibWarning): """Special warning issued when Passlib encounters something that might affect security. .. versionadded:: 1.6 """ #============================================================================= # 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 #============================================================================= passlib-1.6.5/passlib/ext/0000755000175000017500000000000012560246762016555 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/ext/__init__.py0000644000175000017500000000000112214647077020655 0ustar biscuitbiscuit00000000000000 passlib-1.6.5/passlib/ext/django/0000755000175000017500000000000012560246762020017 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/ext/django/__init__.py0000644000175000017500000000034412214647077022131 0ustar biscuitbiscuit00000000000000"""passlib.ext.django.models -- monkeypatch django hashing framework this plugin monkeypatches django's hashing framework so that it uses a passlib context object, allowing handling of arbitrary hashes in Django databases. """ passlib-1.6.5/passlib/ext/django/utils.py0000644000175000017500000004724012555044153021532 0ustar biscuitbiscuit00000000000000"""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 # 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_preset_config", "get_passlib_hasher", ] #============================================================================= # default policies #============================================================================= # map preset names -> passlib.app attrs _preset_map = { "django-1.0": "django10_context", "django-1.4": "django14_context", "django-1.6": "django16_context", "django-latest": "django_context", } 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.6"``). * ``"django-1.0"`` - config used by stock Django 1.0 - 1.3 installs * ``"django-1.4"`` - config used by stock Django 1.4 installs * ``"django-1.6"`` - config used by stock Django 1.6 installs """ # TODO: add preset which includes HASHERS + PREFERRED_HASHERS, # after having imported any custom hashers. e.g. "django-current" if name == "django-default": if not DJANGO_VERSION: raise ValueError("can't resolve django-default preset, " "django not installed") if DJANGO_VERSION < (1,4): name = "django-1.0" elif DJANGO_VERSION < (1,6): name = "django-1.4" else: name = "django-1.6" if name == "passlib-default": return PASSLIB_DEFAULT try: attr = _preset_map[name] except KeyError: raise ValueError("unknown preset config name: %r" % name) import passlib.apps return getattr(passlib.apps, attr).to_string() # default context used by passlib 1.6 PASSLIB_DEFAULT = """ [passlib] ; list of schemes supported by configuration ; currently all django 1.6, 1.4, and 1.0 hashes, ; and three common modular crypt format hashes. schemes = django_pbkdf2_sha256, django_pbkdf2_sha1, django_bcrypt, django_bcrypt_sha256, django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5, sha512_crypt, bcrypt, phpass ; 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 = django_pbkdf2_sha1, django_salted_sha1, django_salted_md5, django_des_crypt, hex_md5 ; 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 """ #============================================================================= # 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):] if hasher_name == "unsalted_sha1": # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes, # but passlib just reuses the "sha1$salt$digest" handler. hasher_name = "sha1" 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 #============================================================================= _GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--" class _HasherWrapper(object): """helper for wrapping passlib handlers in Hasher-compatible class.""" # filled in by subclass, drives the other methods. passlib_handler = None iterations = None @classproperty def algorithm(cls): assert not hasattr(cls.passlib_handler, "django_name") return PASSLIB_HASHER_PREFIX + cls.passlib_handler.name def salt(self): # NOTE: passlib's handler.encrypt() should generate new salt each time, # so this just returns a special constant which tells # encode() (below) not to pass a salt keyword along. return _GEN_SALT_SIGNAL 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 != _GEN_SALT_SIGNAL: kwds['salt'] = salt if iterations is not None: kwds['rounds'] = iterations elif self.iterations is not None: kwds['rounds'] = self.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 from django.utils.translation import ugettext_noop as _ from django.utils.datastructures import 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) # added in django 1.6 def must_update(self, encoded): # TODO: would like to do something useful here, # but would require access to password context, # which would mean a serious recoding of this ext. return False # cache of hasher wrappers generated by get_passlib_hasher() _hasher_cache = WeakKeyDictionary() def get_passlib_hasher(handler, algorithm=None): """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. """ 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 add this to _hasher_cache[] name = handler.django_name if name == "sha1" and algorithm == "unsalted_sha1": # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes, # but passlib just reuses the "sha1$salt$digest" handler. # we want to resolve to correct django hasher. name = algorithm return _get_hasher(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: # we've patched django, so have to use patch manager to retrieve # 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: warn(msg, PasslibRuntimeWarning) #=================================================================== # 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) 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 #============================================================================= passlib-1.6.5/passlib/ext/django/models.py0000644000175000017500000003124712555044153021655 0ustar biscuitbiscuit00000000000000"""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"] #============================================================================= # global attrs #============================================================================= # 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 UNUSABLE_PASSWORD and is_password_usable() 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 elif VERSION < (1,6): has_hashers = True from django.contrib.auth.hashers import UNUSABLE_PASSWORD, \ is_password_usable # NOTE: 1.4 - 1.5 - empty passwords no longer valid. def is_valid_secret(secret): return bool(secret) else: has_hashers = True from django.contrib.auth.hashers import is_password_usable # 1.6 - empty passwords valid again def is_valid_secret(secret): return secret is not None if VERSION < (1,6): def make_unusable_password(): return UNUSABLE_PASSWORD else: from django.contrib.auth.hashers import make_password as _make_password def make_unusable_password(): return _make_password(None) # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes has_unsalted_sha1 = (VERSION >= (1,4,6)) # # 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 = make_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 if not hash and VERSION < (1,4): 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 its 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 make_unusable_password() if hasher == "default": scheme = None else: scheme = hasher_to_passlib_name(hasher) kwds = dict(scheme=scheme) handler = password_context.handler(scheme) if "salt" in handler.setting_kwds: if hasher.startswith("unsalted_"): # Django 1.4.6+ uses a separate 'unsalted_sha1' hasher for "sha1$$digest", # but passlib just reuses it's "sha1" handler ("sha1$salt$digest"). To make # this work, have to explicitly tell the sha1 handler to use an empty salt. kwds['salt'] = '' elif salt: # Django make_password() autogenerates a salt if salt is bool False (None / ''), # so we only pass the keyword on if there's actually a fixed salt. kwds['salt'] = salt 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) # NOTE: resolving scheme -> handler instead of # passing scheme into get_passlib_hasher(), # in case context contains custom handler # shadowing name of a builtin handler. handler = password_context.handler(scheme) return get_passlib_hasher(handler, algorithm=algorithm) # identify_hasher() was added in django 1.5, # patching it anyways for 1.4, so passlib's version is always available. @_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) algorithm = None if (has_unsalted_sha1 and handler.name == "django_salted_sha1" and encoded.startswith("sha1$$")): # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes, # but passlib just reuses the "sha1$salt$digest" handler. # we want to resolve to correct django hasher. algorithm = "unsalted_sha1" return get_passlib_hasher(handler, algorithm=algorithm) _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 # 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 #============================================================================= passlib-1.6.5/passlib/_setup/0000755000175000017500000000000012560246762017254 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/_setup/__init__.py0000644000175000017500000000010012214647077021354 0ustar biscuitbiscuit00000000000000"""passlib.setup - helpers used by passlib's setup.py script""" passlib-1.6.5/passlib/_setup/docdist.py0000644000175000017500000000604512257351267021264 0ustar biscuitbiscuit00000000000000"""custom command to build doc.zip file""" #============================================================================= # imports #============================================================================= # core import os from distutils import dir_util from distutils.cmd import Command from distutils.errors import * from distutils.spawn import spawn # local __all__ = [ "docdist" ] #============================================================================= # 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]"), ('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 self.format = None self.keep_temp = False self.sign = False self.identity = None def finalize_options(self): if self.identity and not self.sign: raise DistutilsOptionError( "Must use --sign for --identity to have meaning" ) if self.build_dir is None: cmd = self.get_finalized_command('build') self.build_dir = os.path.join(cmd.build_base, 'docdist') if not self.dist_dir: 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") cmd = self.get_finalized_command("build_sphinx") source_dir = cmd.builder_target_dir # copy to directory with appropriate name dist = self.distribution arc_name = "%s-docs-%s" % (dist.get_name(), dist.get_version()) tmp_dir = os.path.join(self.build_dir, arc_name) if os.path.exists(tmp_dir): dir_util.remove_tree(tmp_dir, dry_run=self.dry_run) self.copy_tree(source_dir, tmp_dir, preserve_symlinks=True) # make archive from dir arc_base = os.path.join(self.dist_dir, arc_name) self.arc_filename = self.make_archive(arc_base, self.format, self.build_dir) # Sign if requested if self.sign: gpg_args = ["gpg", "--detach-sign", "-a", self.arc_filename] if self.identity: gpg_args[2:2] = ["--local-user", self.identity] spawn(gpg_args, dry_run=self.dry_run) # cleanup if not self.keep_temp: dir_util.remove_tree(tmp_dir, dry_run=self.dry_run) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/_setup/stamp.py0000644000175000017500000000405312555044153020746 0ustar biscuitbiscuit00000000000000"""update version string during build""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import os import re import time from distutils.dist import Distribution # 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) def stamp_source(base_dir, version, dry_run=False): """update version string in passlib dist""" path = os.path.join(base_dir, "passlib", "__init__.py") with open(path) as fh: input = fh.read() output, count = re.subn('(?m)^__version__\s*=.*$', '__version__ = ' + repr(version), input) 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) opts['cmdclass']['build_py'] = build_py # subclass sdist to do same thing _sdist = get_command_class(opts, "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 #============================================================================= passlib-1.6.5/passlib/handlers/0000755000175000017500000000000012560246762017555 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/handlers/fshp.py0000644000175000017500000001646012555044153021070 0ustar biscuitbiscuit00000000000000"""passlib.handlers.fshp """ #============================================================================= # imports #============================================================================= # core from base64 import b64encode, b64decode import re import logging; log = logging.getLogger(__name__) from warnings import warn # 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 # local __all__ = [ 'fshp', ] #============================================================================= # 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:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords: :param salt: Optional raw salt string. If not specified, one will be autogenerated (this is recommended). :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 16 bytes, but can be any non-negative value. :param rounds: Optional number of rounds to use. Defaults to 480000, must be between 1 and 4294967295, inclusive. :param variant: Optionally specifies variant of FSHP to use. * ``0`` - uses SHA-1 digest (deprecated). * ``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 #=================================================================== #--GenericHandler-- name = "fshp" setting_kwds = ("salt", "salt_size", "rounds", "variant") 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 min_salt_size = 0 max_salt_size = None #--HasRounds-- # FIXME: should probably use different default rounds # based on the variant. setting for default variant (sha256) for now. default_rounds = 480000 # 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 name, digest size) 0: ("sha1", 20), 1: ("sha256", 32), 2: ("sha384", 48), 3: ("sha512", 64), } _variant_aliases = dict( [(unicode(k),k) for k in _variant_info] + [(v[0],k) for k,v in iteritems(_variant_info)] ) #=================================================================== # instance attrs #=================================================================== variant = None #=================================================================== # 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) def _norm_variant(self, variant): if variant is None: 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 = 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 self._variant_info: raise ValueError("invalid fshp variant") return variant @property def checksum_alg(self): return self._variant_info[self.variant][0] @property def checksum_size(self): return self._variant_info[self.variant][1] #=================================================================== # 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): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: 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 TypeError: raise uh.exc.MalformedHashError(cls) salt = data[:salt_size] chk = data[salt_size:] 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 or self._stub_checksum salt = self.salt 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): 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. return pbkdf1( secret=self.salt, salt=secret, rounds=self.rounds, keylen=self.checksum_size, hash=self.checksum_alg, ) #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/__init__.py0000644000175000017500000000012612214647077021665 0ustar biscuitbiscuit00000000000000"""passlib.handlers -- holds implementations of all passlib's builtin hash formats""" passlib-1.6.5/passlib/handlers/ldap_digests.py0000644000175000017500000002374212555044153022573 0ustar biscuitbiscuit00000000000000"""passlib.handlers.digests - plain hash digests """ #============================================================================= # 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 # 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", "ldap_sha1", "ldap_salted_md5", "ldap_salted_sha1", ##"get_active_ldap_crypt_schemes", "ldap_des_crypt", "ldap_bsdi_crypt", "ldap_md5_crypt", "ldap_sha1_crypt" "ldap_bcrypt", "ldap_sha256_crypt", "ldap_sha512_crypt", ] #============================================================================= # 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 _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 def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") 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", "salt_size") checksum_chars = uh.PADDED_BASE64_CHARS 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 # 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): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: 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 uascii_to_str(hash) def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") return self._hash_func(secret + self.salt).digest() #============================================================================= # implementations #============================================================================= class ldap_md5(_Base64DigestHelper): """This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. """ name = "ldap_md5" ident = u("{MD5}") _hash_func = md5 _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:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. """ name = "ldap_sha1" ident = u("{SHA}") _hash_func = sha1 _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-16 byte salt. 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 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}") checksum_size = 16 _hash_func = md5 _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-16 byte salt. 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 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}") checksum_size = 20 _hash_func = sha1 _hash_regex = re.compile(u(r"^\{SSHA\}(?P[+/a-zA-Z0-9]{32,}={0,2})$")) _stub_checksum = b('\x00') * 20 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. 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 subclasses plaintext, since all it does differently # is override identify() name = "ldap_plaintext" _2307_pat = re.compile(u(r"^\{\w+\}.*$")) @classmethod 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(): # 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) del g _init_ldap_crypt_handlers() ##_lcn_host = None ##def get_host_ldap_crypt_schemes(): ## global _lcn_host ## if _lcn_host is None: ## from passlib.hosts import host_context ## schemes = host_context.schemes() ## _lcn_host = [ ## "ldap_" + name ## for name in unix_crypt_names ## if name in schemes ## ] ## return _lcn_host #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/oracle.py0000644000175000017500000001533212555044153021372 0ustar biscuitbiscuit00000000000000"""passlib.handlers.oracle - Oracle DB Password Hashes""" #============================================================================= # 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 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 import passlib.utils.handlers as uh # local __all__ = [ "oracle10g", "oracle11g" ] #============================================================================= # oracle10 #============================================================================= def des_cbc_encrypt(key, value, iv=b('\x00') * 8, pad=b('\x00')): """performs des-cbc encryption, returns only last block. this performs a specific DES-CBC encryption implementation as needed by the Oracle10 hash. it probably won't be useful for other purposes as-is. input value is null-padded to multiple of 8 bytes. :arg key: des key as bytes :arg value: value to encrypt, as bytes. :param iv: optional IV :param pad: optional pad byte :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 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 ORACLE10_MAGIC = b("\x01\x23\x45\x67\x89\xAB\xCD\xEF") 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 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 all require the following additional contextual keywords: :type user: str :param user: name of oracle user account this password is associated with. """ #=================================================================== # algorithm information #=================================================================== name = "oracle10" checksum_chars = uh.HEX_CHARS checksum_size = 16 #=================================================================== # methods #=================================================================== @classmethod def _norm_hash(cls, hash): return hash.upper() 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. # # 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 hexlify(hash).decode("ascii").upper() #=================================================================== # 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:`~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 hexadecimal 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 #=================================================================== #--GenericHandler-- name = "oracle11" setting_kwds = ("salt",) checksum_size = 40 checksum_chars = uh.UPPER_HEX_CHARS _stub_checksum = u('0') * 40 #--HasSalt-- min_salt_size = max_salt_size = 20 salt_chars = uh.UPPER_HEX_CHARS #=================================================================== # 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): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") 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 uascii_to_str(hash) def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") chk = sha1(secret + unhexlify(self.salt.encode("ascii"))).hexdigest() return str_to_uascii(chk).upper() #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/scram.py0000644000175000017500000005333112555044153021233 0ustar biscuitbiscuit00000000000000"""passlib.handlers.scram - hash for SCRAM credential storage""" #============================================================================= # imports #============================================================================= # core from binascii import hexlify, unhexlify from base64 import b64encode, b64decode import hashlib import re import logging; log = logging.getLogger(__name__) from warnings import warn # site # pkg from passlib.exc import PasslibHashWarning from passlib.utils import ab64_decode, ab64_encode, consteq, saslprep, \ to_native_str, xor_bytes, splitcomma from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, \ PY3, u, unicode, native_string_types from passlib.utils.pbkdf2 import pbkdf2, get_prf, norm_hash_name import passlib.utils.handlers as uh # local __all__ = [ "scram", ] #============================================================================= # scram credentials hash #============================================================================= class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class provides a format for storing SCRAM passwords, 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: bytes :param salt: Optional salt bytes. If specified, the length must be between 0-1024 bytes. If not specified, a 12 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 12 bytes, but can be any value between 0 and 1024. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 100000, but must be within ``range(1,1<<32)``. :type algs: list of strings :param algs: Specify list of digest algorithms to use. By default each scram hash will contain digests for SHA-1, SHA-256, and SHA-512. This can be overridden by specify either be a list such as ``["sha-1", "sha-256"]``, or a comma-separated string such as ``"sha-1, sha-256"``. Names are case insensitive, and may use :mod:`!hashlib` or `IANA `_ 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 = 100000 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, native_string_types): 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 #============================================================================= passlib-1.6.5/passlib/handlers/roundup.py0000644000175000017500000000223212214647076021621 0ustar biscuitbiscuit00000000000000"""passlib.handlers.roundup - Roundup issue tracker hashes""" #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) # site # pkg import passlib.utils.handlers as uh from passlib.utils.compat import u # local __all__ = [ "roundup_plaintext", "ldap_hex_md5", "ldap_hex_sha1", ] #============================================================================= # #============================================================================= roundup_plaintext = uh.PrefixWrapper("roundup_plaintext", "plaintext", prefix=u("{plaintext}"), lazy=True) # NOTE: these are here because they're currently only known to be used by roundup ldap_hex_md5 = uh.PrefixWrapper("ldap_hex_md5", "hex_md5", u("{MD5}"), lazy=True) ldap_hex_sha1 = uh.PrefixWrapper("ldap_hex_sha1", "hex_sha1", u("{SHA}"), lazy=True) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/des_crypt.py0000644000175000017500000004500212555044153022116 0ustar biscuitbiscuit00000000000000"""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 # 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", "bigcrypt", "crypt16", ] #============================================================================= # pure-python backend for des_crypt family #============================================================================= _BNULL = b('\x00') def _crypt_secret_to_key(secret): """convert secret to 64-bit DES key. 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: 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 raise ValueError("invalid chars in salt") # 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) # 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 = des_encrypt_int_block(key_value, 0, salt_value, 25) # run h64 encode on result return h64big.encode_int64(result) 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 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 def _raw_bsdi_crypt(secret, rounds, salt): """pure-python backend for bsdi_crypt""" # decode salt try: salt_value = h64.decode_int24(salt) except ValueError: # pragma: no cover - always caught by class raise ValueError("invalid salt") # 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:`~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 #=================================================================== #--GenericHandler-- name = "des_crypt" setting_kwds = ("salt",) checksum_chars = uh.HASH64_CHARS checksum_size = 11 #--HasSalt-- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #=================================================================== # formatting #=================================================================== # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum _hash_regex = re.compile(u(r""" ^ (?P[./a-z0-9]{2}) (?P[./a-z0-9]{11})? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") salt, chk = hash[:2], hash[2:] return cls(salt=salt, checksum=chk or None) 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): return test_crypt("test", 'abgOeLfPimXQo') def _calc_checksum_builtin(self, secret): return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): # 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 #=================================================================== 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:`~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 5001, must be between 1 and 16777215, inclusive. :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 #=================================================================== #--GenericHandler-- name = "bsdi_crypt" setting_kwds = ("salt", "rounds") checksum_size = 11 checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 4 salt_chars = uh.HASH64_CHARS #--HasRounds-- default_rounds = 5001 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. #=================================================================== # 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 from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: 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, ) 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): return test_crypt("test", '_/...lLDAxARksGCHin.') def _calc_checksum_builtin(self, secret): return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): 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 #=================================================================== 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:`~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 #=================================================================== #--GenericHandler-- name = "bigcrypt" setting_kwds = ("salt",) checksum_chars = uh.HASH64_CHARS # NOTE: checksum chars must be multiple of 11 #--HasSalt-- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #=================================================================== # internal helpers #=================================================================== _hash_regex = re.compile(u(r""" ^ (?P[./a-z0-9]{2}) (?P([./a-z0-9]{11})+)? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk) def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) return uascii_to_str(hash) def _norm_checksum(self, value): value = super(bigcrypt, self)._norm_checksum(value) if value and len(value) % 11: raise uh.exc.InvalidHashError(self) return value #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") chk = _raw_des_crypt(secret, self.salt.encode("ascii")) idx = 8 end = len(secret) while idx < end: next = idx + 8 chk += _raw_des_crypt(secret[idx:next], chk[-11:-9]) idx = next return chk.decode("ascii") #=================================================================== # 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:`~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 #=================================================================== #--GenericHandler-- name = "crypt16" setting_kwds = ("salt",) checksum_size = 22 checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #=================================================================== # internal helpers #=================================================================== _hash_regex = re.compile(u(r""" ^ (?P[./a-z0-9]{2}) (?P[./a-z0-9]{22})? $"""), re.X|re.I) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk) 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 try: salt_value = h64.decode_int12(self.salt.encode("ascii")) except ValueError: # pragma: no cover - caught by class raise ValueError("invalid chars in salt") # 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 = 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:16]) # run data through des using input of 0 result2 = des_encrypt_int_block(key2, 0, salt_value, 5) # done chk = h64big.encode_int64(result1) + h64big.encode_int64(result2) return chk.decode("ascii") #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/sha1_crypt.py0000644000175000017500000001231712555044153022202 0ustar biscuitbiscuit00000000000000"""passlib.handlers.sha1_crypt """ #============================================================================= # imports #============================================================================= # core from hmac import new as hmac from hashlib import sha1 import re import logging; log = logging.getLogger(__name__) from warnings import warn # 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 #============================================================================= _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:`~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 480000, must be between 1 and 4294967295, inclusive. :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 #=================================================================== #--GenericHandler-- name = "sha1_crypt" setting_kwds = ("salt", "salt_size", "rounds") ident = u("$sha1$") checksum_size = 28 checksum_chars = uh.HASH64_CHARS #--HasSalt-- default_salt_size = 8 min_salt_size = 0 max_salt_size = 64 salt_chars = uh.HASH64_CHARS #--HasRounds-- default_rounds = 480000 # current passlib default min_rounds = 1 # really, this should be higher. max_rounds = 4294967295 # 32-bit integer limit rounds_cost = "linear" #=================================================================== # formatting #=================================================================== @classmethod def from_string(cls, hash): 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): 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 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) r += 1 return h64.encode_transposed_bytes(result, self._chk_offsets).decode("ascii") _chk_offsets = [ 2,1,0, 5,4,3, 8,7,6, 11,10,9, 14,13,12, 17,16,15, 0,19,18, ] def _calc_checksum_os_crypt(self, secret): 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 #============================================================================= passlib-1.6.5/passlib/handlers/bcrypt.py0000644000175000017500000006067212555044153021437 0ustar biscuitbiscuit00000000000000"""passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm. TODO: * support 2x and altered-2a hashes? http://www.openwall.com/lists/oss-security/2011/06/27/9 * deal with lack of PY3-compatibile c-ext implementation """ #============================================================================= # imports #============================================================================= from __future__ import with_statement, absolute_import # core from base64 import b64encode from hashlib import sha256 import os import re import logging; log = logging.getLogger(__name__) from warnings import warn # site try: import bcrypt as _bcrypt except ImportError: # pragma: no cover _bcrypt = None try: import bcryptor as _bcryptor except ImportError: # pragma: no cover _bcryptor = None # pkg from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError from passlib.utils import bcrypt64, safe_crypt, repeat_string, to_bytes, \ classproperty, rng, getrandstr, test_crypt, to_unicode from passlib.utils.compat import bytes, b, u, uascii_to_str, unicode, str_to_uascii import passlib.utils.handlers as uh # local __all__ = [ "bcrypt", ] #============================================================================= # 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$") IDENT_2B = u("$2b$") _BNULL = b('\x00') def _detect_pybcrypt(): """ internal helper which tries to distinguish pybcrypt vs bcrypt. :returns: True if cext-based py-bcrypt, False if ffi-based bcrypt, None if 'bcrypt' module not found. .. versionchanged:: 1.6.3 Now assuming bcrypt installed, unless py-bcrypt explicitly detected. Previous releases assumed py-bcrypt by default. Making this change since py-bcrypt is (apparently) unmaintained and static, whereas bcrypt is being actively maintained, and it's internal structure may shift. """ # NOTE: this is also used by the unittests. # check for module. try: import bcrypt except ImportError: return None # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4), # which bcrypt lacks (confirmed for v1.0 - 2.0) # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute. try: from bcrypt._bcrypt import __version__ except ImportError: return False return True #============================================================================= # 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:`~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}` -- increasing the rounds by +1 will double the amount of time taken. :type ident: str :param ident: 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"`` - some implementations suffered from a very rare security flaw. current default for compatibility purposes. * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, identical to ``"2a"`` in all but name. * ``"2b"`` - latest revision of the official BCrypt algorithm (will be default in Passlib 1.7). :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. .. versionchanged:: 1.6.3 Added support for ``"2b"`` variant. """ #=================================================================== # class attrs #=================================================================== #--GenericHandler-- name = "bcrypt" setting_kwds = ("salt", "rounds", "ident") checksum_size = 31 checksum_chars = bcrypt64.charmap #--HasManyIdents-- default_ident = IDENT_2A ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B) ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y, u("2b"): IDENT_2B} #--HasSalt-- min_salt_size = max_salt_size = 22 salt_chars = bcrypt64.charmap # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap #--HasRounds-- default_rounds = 12 # current passlib default min_rounds = 4 # minimum from bcrypt specification max_rounds = 31 # 32-bit integer limit (since real_rounds=1< # NOTE: reference hash taken from above url, and is the incorrectly generate 2x hash. if cls.verify(u("\xA3"), "$2a$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"): raise PasslibSecurityError( "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to " "the crypt_blowfish 8-bit bug (CVE-2011-2483), " "and should be upgraded or replaced with another backend." % backend) # check for bsd wraparound bug (fixed in 2b) # this is treated as a warning, because it's rare in the field, # and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it. # test cases from # NOTE: reference hash is of password "0"*72 # NOTE: if in future we need to deliberately create hashes which have this bug, # can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)' cls._has_wraparound_bug = False if cls.verify(("0123456789"*26)[:255], "$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"): warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to " "the bsd wraparound bug, " "and should be upgraded or replaced with another backend " "(this warning will be fatal under passlib 1.7)" % backend) cls._has_wraparound_bug = True def _detect_lacks_variant(ident, refhash): """helper to detect if backend *lacks* support for specified bcrypt variant""" assert refhash.startswith(ident) # NOTE: can't use cls.verify() directly or we have recursion error try: result = cls.verify("test", refhash) except (ValueError, _bcryptor.engine.SaltError if _bcryptor else ValueError): # backends without support will throw various errors about unrecognized version # pybcrypt, bcrypt -- raises ValueError # bcryptor -- raises bcryptor.engine.SaltError log.debug("%r backend lacks %r support", backend, ident) return True assert result, "%r backend %r check failed" % (backend, ident) return False # check for native 2 support # NOTE: have to clear workaround first, so verify() doesn't enable it during detection. cls._lacks_20_support = False cls._lacks_20_support = _detect_lacks_variant("$2$", "$2$04$5BJqKfqMQvV7nS.yUguNcu" "RfMMOXK0xPWavM7pOzjEi5ze5T1k8/S") # TODO: check for 2x support # check for native 2y support cls._lacks_2y_support = False cls._lacks_2y_support = _detect_lacks_variant("$2y$", "$2y$04$5BJqKfqMQvV7nS.yUguNcu" "eVirQqDBGaLXSqj.rs.pZPlNR0UX/HK") # check for native 2b support cls._lacks_2b_support = False cls._lacks_2b_support = _detect_lacks_variant("$2b$", "$2b$04$5BJqKfqMQvV7nS.yUguNcu" "eVirQqDBGaLXSqj.rs.pZPlNR0UX/HK") # sanity check assert cls._lacks_2b_support or not cls._has_wraparound_bug, \ "sanity check failed: %r backend supports $2b$ but has wraparound bug" % backend @classproperty def _has_backend_bcrypt(cls): return _bcrypt is not None and not _detect_pybcrypt() @classproperty def _has_backend_pybcrypt(cls): return _bcrypt is not None and _detect_pybcrypt() @classproperty def _has_backend_bcryptor(cls): return _bcryptor is not None @classproperty def _has_backend_builtin(cls): if os.environ.get("PASSLIB_BUILTIN_BCRYPT") not in ["enable","enabled"]: return False # look at it cross-eyed, and it loads itself _load_builtin() return True @classproperty def _has_backend_os_crypt(cls): # XXX: what to do if "2" isn't supported, but "2a" is? # "2" is *very* rare, and can fake it using "2a"+repeat_string h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' return test_crypt("test",h1) and test_crypt("test", h2) @classmethod def _no_backends_msg(cls): return "no bcrypt backends available -- recommend you install one (e.g. 'pip install bcrypt')" def _calc_checksum(self, secret): """common backend code""" # make sure it's unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") # NOTE: especially important to forbid NULLs for bcrypt, since many # backends (bcryptor, bcrypt) happily accept them, and then # silently truncate the password at first NULL they encounter! if _BNULL in secret: raise uh.exc.NullPasswordError(self) # ensure backend is loaded before workaround detection self.get_backend() # protect from wraparound bug by truncating secret before handing it to the backend. # bcrypt only uses first 72 bytes anyways. if self._has_wraparound_bug and len(secret) >= 255: secret = secret[:72] # special case handling for variants (ordered most common first) ident = self.ident if ident == IDENT_2A: # fall through and use backend w/o hacks pass elif ident == IDENT_2B: if self._lacks_2b_support: # handle $2b$ hash format even if backend is too old. # have it generate a 2A digest, then return it as a 2B hash. ident = IDENT_2A elif ident == IDENT_2Y: if self._lacks_2y_support: # handle $2y$ hash format (not supported by BSDs, being phased out on others) # have it generate a 2A digest, then return it as a 2Y hash. ident = IDENT_2A elif ident == IDENT_2: if self._lacks_20_support: # handle legacy $2$ format (not supported by most backends except BSD os_crypt) # 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) ident = IDENT_2A elif ident == IDENT_2X: # NOTE: shouldn't get here. # XXX: could check if backend does actually offer 'support' raise RuntimeError("$2x$ hashes not currently supported by passlib") else: raise AssertionError("unexpected ident value: %r" % ident) # invoke backend config = self._get_config(ident) return self._calc_checksum_backend(secret, config) def _calc_checksum_os_crypt(self, secret, config): hash = safe_crypt(secret, config) if hash: assert hash.startswith(config) and len(hash) == len(config)+31 return hash[-31:] else: # NOTE: Have to raise this error because python3's crypt.crypt() only accepts unicode. # This means it can't handle any passwords that aren't either unicode # or utf-8 encoded bytes. However, hashing a password with an alternate # encoding should be a pretty rare edge case; if user needs it, they can just # install bcrypt backend. # XXX: is this the right error type to raise? # maybe have safe_crypt() not swallow UnicodeDecodeError, and have handlers # like sha256_crypt trap it if they have alternate method of handling them? raise uh.exc.MissingBackendError( "non-utf8 encoded passwords can't be handled by crypt.crypt() under python3, " "recommend running `pip install bcrypt`.", ) def _calc_checksum_bcrypt(self, secret, config): # bcrypt behavior: # hash must be ascii bytes # secret must be bytes # returns bytes if isinstance(config, unicode): config = config.encode("ascii") hash = _bcrypt.hashpw(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 assert isinstance(hash, bytes) return hash[-31:].decode("ascii") def _calc_checksum_pybcrypt(self, secret, config): # py-bcrypt behavior: # py2: unicode secret/hash encoded as ascii bytes before use, # bytes taken as-is; returns ascii bytes. # py3: unicode secret encoded as utf-8 bytes, # hash encoded as ascii bytes, returns ascii unicode. hash = _bcrypt.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, config): # bcryptor behavior: # py2: unicode secret/hash encoded as ascii bytes before use, # bytes taken as-is; returns ascii bytes. # py3: not supported hash = _bcryptor.engine.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, config): chk = _builtin_bcrypt(secret, config[1:config.index("$", 1)], self.salt.encode("ascii"), self.rounds) return chk.decode("ascii") #=================================================================== # eoc #=================================================================== _UDOLLAR = u("$") class bcrypt_sha256(bcrypt): """This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`. It supports a fixed-length salt, and a variable number of rounds. The :meth:`~passlib.ifc.PasswordHash.encrypt` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept all the same optional keywords as the base :class:`bcrypt` hash. .. versionadded:: 1.6.2 """ name = "bcrypt_sha256" # this is locked at 2a for now. ident_values = (IDENT_2A,) # sample hash: # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu # $bcrypt-sha256$ -- prefix/identifier # 2a -- bcrypt variant # , -- field separator # 6 -- bcrypt work factor # $ -- section separator # /3OeRpbOf8/l6nPPRdZPp. -- salt # $ -- section separator # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest # XXX: we can't use .ident attr due to bcrypt code using it. # working around that via prefix. prefix = u('$bcrypt-sha256$') _hash_re = re.compile(r""" ^ [$]bcrypt-sha256 [$](?P[a-z0-9]+) ,(?P\d{1,2}) [$](?P[^$]{22}) ([$](?P.{31}))? $ """, re.X) @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) if not hash: return False return hash.startswith(cls.prefix) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") if not hash.startswith(cls.prefix): raise uh.exc.InvalidHashError(cls) m = cls._hash_re.match(hash) if not m: raise uh.exc.MalformedHashError(cls) rounds = m.group("rounds") if rounds.startswith(uh._UZERO) and rounds != uh._UZERO: raise uh.exc.ZeroPaddedRoundsError(cls) return cls(ident=m.group("variant"), rounds=int(rounds), salt=m.group("salt"), checksum=m.group("digest"), ) def to_string(self): hash = u("%s%s,%d$%s") % (self.prefix, self.ident.strip(_UDOLLAR), self.rounds, self.salt) if self.checksum: hash = u("%s$%s") % (hash, self.checksum) return uascii_to_str(hash) def _calc_checksum(self, secret): # NOTE: can't use digest directly, since bcrypt stops at first NULL. # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password # (XXX: citation needed), so we don't want key to be > 55 bytes. # thus, have to use base64 (44 bytes) rather than hex (64 bytes). # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256 # may switch to hex encoding, since it's simpler to implement elsewhere. if isinstance(secret, unicode): secret = secret.encode("utf-8") key = b64encode(sha256(secret).digest()) # hand result off to normal bcrypt algorithm return super(bcrypt_sha256, self)._calc_checksum(key) # patch set_backend so it modifies bcrypt class, not this one... # else the bcrypt.set_backend() tests will call the wrong class. @classmethod def set_backend(cls, *args, **kwds): return bcrypt.set_backend(*args, **kwds) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/md5_crypt.py0000644000175000017500000003156212555044153022036 0ustar biscuitbiscuit00000000000000"""passlib.handlers.md5_crypt - md5-crypt algorithm""" #============================================================================= # imports #============================================================================= # core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn # 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 #============================================================================= _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), ) # 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) def _raw_md5_crypt(pwd, salt, use_apr=False): """perform raw md5-crypt calculation 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 chars """ # 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. #=================================================================== # 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. #=================================================================== # NOTE: the original MD5-Crypt implementation performs the C digest # calculation using the following loop: # ##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 # # 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', 'salt', and 'pwd'; and the exact # combination is determined by 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 salt & pwd for each round 0-41 # * runs through as many 42-round blocks as possible (23) # * runs through as many pairs of rounds as needed for remaining rounds (17) # * this results in the required 42*23+2*17=1000 rounds required by md5_crypt. # # this cuts out a lot of the control overhead incurred when running the # original loop 1000 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""" #=================================================================== # class attrs #=================================================================== # name - set in subclass setting_kwds = ("salt", "salt_size") # ident - set in subclass checksum_size = 22 checksum_chars = uh.HASH64_CHARS min_salt_size = 0 max_salt_size = 8 salt_chars = uh.HASH64_CHARS #=================================================================== # methods #=================================================================== @classmethod 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) # _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:`~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 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. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== name = "md5_crypt" ident = u("$1$") #=================================================================== # methods #=================================================================== # FIXME: can't find definitive policy on how md5-crypt handles non-ascii. # all backends currently coerce -> utf-8 backends = ("os_crypt", "builtin") _has_backend_builtin = True @classproperty def _has_backend_os_crypt(cls): return test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/') def _calc_checksum_builtin(self, secret): return _raw_md5_crypt(secret, self.salt) def _calc_checksum_os_crypt(self, secret): 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 #=================================================================== 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:`~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 """ #=================================================================== # class attrs #=================================================================== name = "apr_md5_crypt" ident = u("$apr1$") #=================================================================== # methods #=================================================================== def _calc_checksum(self, secret): return _raw_md5_crypt(secret, self.salt, use_apr=True) #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/sha2_crypt.py0000644000175000017500000004641212555044153022206 0ustar biscuitbiscuit00000000000000"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt""" #============================================================================= # imports #============================================================================= # core import hashlib import logging; log = logging.getLogger(__name__) from warnings import warn # 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__ = [ "sha512_crypt", "sha256_crypt", ] #============================================================================= # 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, ) # 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, ) def _raw_sha2_crypt(pwd, salt, rounds, use_512=False): """perform raw sha256-crypt / sha512-crypt 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 :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. #=================================================================== # 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 - set by subclass checksum_chars = uh.HASH64_CHARS # checksum_size - set by subclass min_salt_size = 0 max_salt_size = 16 salt_chars = uh.HASH64_CHARS min_rounds = 1000 # bounds set by spec max_rounds = 999999999 # bounds set by spec rounds_cost = "linear" _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 = (self.use_defaults and self.rounds == 5000) self.implicit_rounds = implicit_rounds @classmethod def from_string(cls, hash): # 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( 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): if self.rounds == 5000 and self.implicit_rounds: hash = u("%s%s$%s") % (self.ident, self.salt, self.checksum or u('')) else: 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 # _has_backend_os_crypt - provided by subclass def _calc_checksum_builtin(self, secret): return _raw_sha2_crypt(secret, self.salt, self.rounds, self._cdb_use_512) def _calc_checksum_os_crypt(self, secret): 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. 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 #=================================================================== 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:`~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 535000, 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. 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. :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 #=================================================================== name = "sha256_crypt" ident = u("$5$") checksum_size = 43 # NOTE: using 25/75 weighting of builtin & os_crypt backends default_rounds = 535000 #=================================================================== # backends #=================================================================== @classproperty def _has_backend_os_crypt(cls): return test_crypt("test", "$5$rounds=1000$test$QmQADEXMG8POI5W" "Dsaeho0P36yK3Tcrgboabng6bkb/") #=================================================================== # eoc #=================================================================== #============================================================================= # sha 512 crypt #============================================================================= class sha512_crypt(_SHA2_Common): """This class implements the SHA512-Crypt password 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, 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 656000, 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. 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. :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 #=================================================================== name = "sha512_crypt" ident = u("$6$") checksum_size = 86 _cdb_use_512 = True # NOTE: using 25/75 weighting of builtin & os_crypt backends default_rounds = 656000 #=================================================================== # backend #=================================================================== @classproperty def _has_backend_os_crypt(cls): return test_crypt("test", "$6$rounds=1000$test$2M/Lx6Mtobqj" "Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn" "yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG" "I5c7TZauS0") #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/mssql.py0000644000175000017500000002101412555044153021256 0ustar biscuitbiscuit00000000000000"""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 #============================================================================= passlib-1.6.5/passlib/handlers/digests.py0000644000175000017500000001233412555044153021566 0ustar biscuitbiscuit00000000000000"""passlib.handlers.digests - plain hash digests """ #============================================================================= # imports #============================================================================= # core import hashlib import logging; log = logging.getLogger(__name__) from warnings import warn # 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 # local __all__ = [ "create_hex_hash", "hex_md4", "hex_md5", "hex_sha1", "hex_sha256", "hex_sha512", ] #============================================================================= # helpers for hexadecimal hashes #============================================================================= class HexDigestHash(uh.StaticHandler): """this provides a template for supporting passwords stored as plain hexadecimal hashes""" #=================================================================== # 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 _norm_hash(cls, hash): return hash.lower() def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") return str_to_uascii(self._hash_func(secret).hexdigest()) #=================================================================== # eoc #=================================================================== 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, __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 hexadecimal %s hash, and follows the :ref:`password-hash-api`. It supports no optional or contextual keywords. """ % (digest_name,) )) #============================================================================= # 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") #============================================================================= # 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 #============================================================================= passlib-1.6.5/passlib/handlers/windows.py0000644000175000017500000002662312555044153021624 0ustar biscuitbiscuit00000000000000"""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 hexadecimal, 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 hexadecimal 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 hexadecimal 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 #============================================================================= passlib-1.6.5/passlib/handlers/postgres.py0000644000175000017500000000442312555044153021772 0ustar biscuitbiscuit00000000000000"""passlib.handlers.postgres_md5 - MD5-based algorithm used by Postgres for pg_shadow table""" #============================================================================= # imports #============================================================================= # core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn # site # pkg from passlib.utils import to_bytes from passlib.utils.compat import b, bytes, str_to_uascii, unicode, u import passlib.utils.handlers as uh # local __all__ = [ "postgres_md5", ] #============================================================================= # handler #============================================================================= class postgres_md5(uh.HasUserContext, uh.StaticHandler): """This class implements the Postgres MD5 Password hash, 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 all require the following additional contextual keywords: :type user: str :param user: name of postgres user account this password is associated with. """ #=================================================================== # algorithm information #=================================================================== name = "postgres_md5" _hash_prefix = u("md5") checksum_chars = uh.HEX_CHARS checksum_size = 32 #=================================================================== # primary interface #=================================================================== def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") user = to_bytes(self.user, "utf-8", param="user") return str_to_uascii(md5(secret + user).hexdigest()) #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/django.py0000644000175000017500000004405312555044153021371 0ustar biscuitbiscuit00000000000000"""passlib.handlers.django- Django password hash support""" #============================================================================= # imports #============================================================================= # core from base64 import b64encode from binascii import hexlify from hashlib import md5, sha1, sha256 import re import logging; log = logging.getLogger(__name__) from warnings import warn # site # pkg from passlib.hash import bcrypt, pbkdf2_sha1, pbkdf2_sha256 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 & constants #============================================================================= # imported by django_des_crypt._calc_checksum() des_crypt = None def _import_des_crypt(): global des_crypt if des_crypt is None: from passlib.hash import des_crypt return des_crypt # 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""" # name, ident, checksum_size must be set by subclass. # ident must include "$" suffix. setting_kwds = ("salt", "salt_size") min_salt_size = 0 # 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 = SALT_CHARS checksum_chars = uh.LOWER_HEX_CHARS @classproperty def _stub_checksum(cls): return cls.checksum_chars[0] * cls.checksum_size @classmethod 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): rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) return cls(rounds=rounds, salt=salt, checksum=chk) def to_string(self): 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:`~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. 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" django_name = "sha1" ident = u("sha1$") checksum_size = 40 def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") 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:`~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. 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" django_name = "md5" ident = u("md5$") checksum_size = 32 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_bcrypt_sha256(bcrypt): """This class implements Django 1.6's Bcrypt+SHA256 hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, and a variable number of rounds. While the algorithm and format is somewhat different, the api and options for this hash are identical to :class:`!bcrypt` itself, see :doc:`/lib/passlib.hash.bcrypt` for more details. .. versionadded:: 1.6.2 """ name = "django_bcrypt_sha256" django_name = "bcrypt_sha256" _digest = sha256 # NOTE: django bcrypt ident locked at "$2a$", so omitting 'ident' support. setting_kwds = ("salt", "rounds") # sample hash: # bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu # XXX: we can't use .ident attr due to bcrypt code using it. # working around that via django_prefix django_prefix = u('bcrypt_sha256$') @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) if not hash: return False return hash.startswith(cls.django_prefix) @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") if not hash.startswith(cls.django_prefix): raise uh.exc.InvalidHashError(cls) bhash = hash[len(cls.django_prefix):] if not bhash.startswith("$2"): raise uh.exc.MalformedHashError(cls) return super(django_bcrypt_sha256, cls).from_string(bhash) def __init__(self, **kwds): if 'ident' in kwds and kwds.get("use_defaults"): raise TypeError("%s does not support the ident keyword" % self.__class__.__name__) return super(django_bcrypt_sha256, self).__init__(**kwds) def to_string(self): bhash = super(django_bcrypt_sha256, self).to_string() return uascii_to_str(self.django_prefix) + bhash def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") secret = hexlify(self._digest(secret).digest()) return super(django_bcrypt_sha256, self)._calc_checksum(secret) # patch set_backend so it modifies bcrypt class, not this one... # else it would clobber our _calc_checksum() wrapper above. @classmethod def set_backend(cls, *args, **kwds): return bcrypt.set_backend(*args, **kwds) 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 29000, 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 = pbkdf2_sha256.default_rounds # NOTE: django 1.6 uses 12000 _prf = "hmac-sha256" def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") 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. :type rounds: int :param rounds: Optional number of rounds to use. Defaults to 131000, 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:`!PBKDF2SHA1PasswordHasher` class. .. versionadded:: 1.6 """ name = "django_pbkdf2_sha1" django_name = "pbkdf2_sha1" ident = u('pbkdf2_sha1$') checksum_size = 28 # 20 bytes -> base64 default_rounds = pbkdf2_sha1.default_rounds # NOTE: django 1.6 uses 12000 _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:`~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]``. 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" 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 # NOTE: regarding duplicate salt field: # # django 1.0 had a "crypt$$" hash format, # used [a-z0-9] to generate a 5 char salt, stored it in salt1, # duplicated the first two chars of salt1 as salt2. # it would throw an error if salt1 was empty. # # django 1.4 started generating 2 char salt using the full alphabet, # left salt1 empty, and only paid attention to salt2. # # in order to be compatible with django 1.0, the hashes generated # by this function will always include salt1, unless the following # class-level field is disabled (mainly used for testing) use_duplicate_salt = True @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) def to_string(self): salt = self.salt chk = salt[:2] + (self.checksum or self._stub_checksum) if self.use_duplicate_salt: # filling in salt field, so that we're compatible with django 1.0 return uh.render_mc2(self.ident, salt, chk) else: # django 1.4+ style hash return uh.render_mc2(self.ident, "", chk) 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() 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`. This class does not implement a hash, but instead claims the special hash string ``"!"`` which Django uses to indicate an account's password has been disabled. * newly encrypted passwords will hash to ``"!"``. * it rejects all passwords. .. note:: Django 1.6 prepends a randomly generate 40-char alphanumeric string to each unusuable password. This class recognizes such strings, but for backwards compatibility, still returns ``"!"``. .. versionchanged:: 1.6.2 added Django 1.6 support """ name = "django_disabled" @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) return hash.startswith(u("!")) def _calc_checksum(self, secret): return u("!") @classmethod def verify(cls, secret, hash): uh.validate_secret(secret) if not cls.identify(hash): raise uh.exc.InvalidHashError(cls) return False #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/pbkdf2.py0000644000175000017500000004620612555044153021301 0ustar biscuitbiscuit00000000000000"""passlib.handlers.pbkdf - PBKDF2 based hashes""" #============================================================================= # 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 # 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 import passlib.utils.handlers as uh # local __all__ = [ "pbkdf2_sha1", "pbkdf2_sha256", "pbkdf2_sha512", "cta_pbkdf2_sha1", "dlitz_pbkdf2_sha1", "grub_pbkdf2_sha512", ] #============================================================================= # #============================================================================= class Pbkdf2DigestHandler(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """base class for various pbkdf2_{digest} algorithms""" #=================================================================== # class attrs #=================================================================== #--GenericHandler-- setting_kwds = ("salt", "salt_size", "rounds") checksum_chars = uh.HASH64_CHARS #--HasSalt-- default_salt_size = 16 min_salt_size = 0 max_salt_size = 1024 #--HasRounds-- default_rounds = None # set by subclass min_rounds = 1 max_rounds = 0xffffffff # setting at 32-bit limit for now rounds_cost = "linear" #--this class-- _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: 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): 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 = ab64_encode(self.salt).decode("ascii") if withchk and self.checksum: chk = ab64_encode(self.checksum).decode("ascii") else: chk = None return uh.render_mc3(self.ident, self.rounds, salt, chk) 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, 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,) 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:`~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)``. :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, 131000, ident=u("$pbkdf2$")) pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32, 29000) pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64, 25000) 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 CTA_ALTCHARS = b("-_") class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """This class implements Cryptacular'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:`~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 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 #=================================================================== #--GenericHandler-- name = "cta_pbkdf2_sha1" setting_kwds = ("salt", "salt_size", "rounds") ident = u("$p5k2$") # 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 = pbkdf2_sha1.default_rounds 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 @classmethod def from_string(cls, hash): # 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) def to_string(self, withchk=True): salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii") if withchk and self.checksum: 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 #============================================================================= 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:`~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 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 #=================================================================== #--GenericHandler-- name = "dlitz_pbkdf2_sha1" setting_kwds = ("salt", "salt_size", "rounds") ident = u("$p5k2$") # 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.HASH64_CHARS #--HasRounds-- # NOTE: for security, the default here is set to match pbkdf2_sha1, # even though this hash's extra block makes it twice as slow. default_rounds = pbkdf2_sha1.default_rounds 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 @classmethod def from_string(cls, hash): 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) 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 = str_to_bascii(self.to_string(withchk=False)) result = pbkdf2(secret, salt, self.rounds, 24, "hmac-sha1") return ab64_encode(result).decode("ascii") #=================================================================== # 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:`~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}") checksum_size = 32 _stub_checksum = b("\x00") * 32 #--HasRawSalt-- min_salt_size = max_salt_size = 16 @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") ident = cls.ident if not hash.startswith(ident): raise uh.exc.InvalidHashError(cls) data = b64decode(hash[len(ident):].encode("ascii")) salt, chk = data[:16], data[16:] 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 uascii_to_str(hash) 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. return pbkdf2(secret, self.salt, 10000, 32, "hmac-sha1") #============================================================================= # 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:`~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 19000, 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.") # 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 = pbkdf2_sha512.default_rounds min_rounds = 1 max_rounds = 0xffffffff # setting at 32-bit limit for now rounds_cost = "linear" @classmethod def from_string(cls, hash): 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() else: 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 if isinstance(secret, unicode): secret = secret.encode("utf-8") return pbkdf2(secret, self.salt, self.rounds, 64, "hmac-sha512") #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/phpass.py0000644000175000017500000001141212555044153021416 0ustar biscuitbiscuit00000000000000"""passlib.handlers.phpass - PHPass Portable Crypt phppass located - http://www.openwall.com/phpass/ algorithm described - http://www.openwall.com/articles/PHP-Users-Passwords phpass context - blowfish, bsdi_crypt, phpass """ #============================================================================= # imports #============================================================================= # core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn # 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 #============================================================================= 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:`~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 19, 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 its 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 #=================================================================== #--GenericHandler-- name = "phpass" setting_kwds = ("salt", "rounds", "ident") checksum_chars = uh.HASH64_CHARS #--HasSalt-- min_salt_size = max_salt_size = 8 salt_chars = uh.HASH64_CHARS #--HasRounds-- default_rounds = 19 min_rounds = 7 max_rounds = 30 rounds_cost = "log2" #--HasManyIdents-- 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$ # 9 # IQRaTwmf # eRo7ud9Fh4E2PdI0S3r.L0 @classmethod def from_string(cls, hash): 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 or None, ) def to_string(self): hash = u("%s%s%s%s") % (self.ident, h64.encode_int6(self.rounds).decode("ascii"), self.salt, 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. if isinstance(secret, unicode): secret = secret.encode("utf-8") real_rounds = 1<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 #============================================================================= passlib-1.6.5/passlib/handlers/sun_md5_crypt.py0000644000175000017500000003322212555044153022716 0ustar biscuitbiscuit00000000000000"""passlib.handlers.sun_md5_crypt - Sun's Md5 Crypt, used on Solaris .. warning:: This implementation may not reproduce the original Solaris behavior in some border cases. See documentation for details. """ #============================================================================= # imports #============================================================================= # core from hashlib import md5 import re import logging; log = logging.getLogger(__name__) from warnings import warn # 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 # exact bytes as in http://www.ibiblio.org/pub/docs/books/gutenberg/etext98/2ws2610.txt # from Project Gutenberg. MAGIC_HAMLET = b( "To be, or not to be,--that is the question:--\n" "Whether 'tis nobler in the mind to suffer\n" "The slings and arrows of outrageous fortune\n" "Or to take arms against a sea of troubles,\n" "And by opposing end them?--To die,--to sleep,--\n" "No more; and by a sleep to say we end\n" "The heartache, and the thousand natural shocks\n" "That flesh is heir to,--'tis a consummation\n" "Devoutly to be wish'd. To die,--to sleep;--\n" "To sleep! perchance to dream:--ay, there's the rub;\n" "For in that sleep of death what dreams may come,\n" "When we have shuffled off this mortal coil,\n" "Must give us pause: there's the respect\n" "That makes calamity of so long life;\n" "For who would bear the whips and scorns of time,\n" "The oppressor's wrong, the proud man's contumely,\n" "The pangs of despis'd love, the law's delay,\n" "The insolence of office, and the spurns\n" "That patient merit of the unworthy takes,\n" "When he himself might his quietus make\n" "With a bare bodkin? who would these fardels bear,\n" "To grunt and sweat under a weary life,\n" "But that the dread of something after death,--\n" "The undiscover'd country, from whose bourn\n" "No traveller returns,--puzzles the will,\n" "And makes us rather bear those ills we have\n" "Than fly to others that we know not of?\n" "Thus conscience does make cowards of us all;\n" "And thus the native hue of resolution\n" "Is sicklied o'er with the pale cast of thought;\n" "And enterprises of great pith and moment,\n" "With this regard, their currents turn awry,\n" "And lose the name of action.--Soft you now!\n" "The fair Ophelia!--Nymph, in thy orisons\n" "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 = 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 ] del xr def raw_sun_md5_crypt(secret, rounds, salt): """given secret & salt, return encoded sun-md5-crypt checksum""" global MAGIC_HAMLET assert isinstance(secret, bytes) assert isinstance(salt, bytes) # validate rounds if rounds <= 0: rounds = 0 real_rounds = 4096 + rounds # 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 "$" result = md5(secret + salt).digest() assert len(result) == 16 # 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 round = 0 while round < real_rounds: # 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 x = 0 xrounds = X_ROUNDS_1 if (rval((round>>3) & 15)>>(round & 7)) & 1 else X_ROUNDS_0 for i, ia, ib in xrounds: a = rval(ia) b = rval(ib) 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 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: a = rval(ia) b = rval(ib) 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" coin = ((rval(x>>3) >> (x&7)) ^ (rval(y>>3) >> (y&7))) & 1 # construct hash for this round h = md5(result) if coin: h.update(MAGIC_HAMLET) h.update(unicode(round).encode("ascii")) result = h.digest() round += 1 # encode output return h64.encode_transposed_bytes(result, _chk_offsets) # NOTE: same offsets as md5_crypt _chk_offsets = ( 12,6,0, 13,7,1, 14,8,2, 15,9,3, 5,10,4, 11, ) #============================================================================= # 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:`~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 34000, 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 #=================================================================== name = "sun_md5_crypt" setting_kwds = ("salt", "rounds", "bare_salt", "salt_size") checksum_chars = uh.HASH64_CHARS checksum_size = 22 # NOTE: docs say max password length is 255. # release 9u2 # 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.HASH64_CHARS default_rounds = 34000 # 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? rounds_cost = "linear" 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 #=================================================================== @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) return hash.startswith(cls.ident_values) @classmethod def from_string(cls, hash): 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 # if hash.startswith(u("$md5$")): rounds = 0 salt_idx = 5 elif hash.startswith(u("$md5,rounds=")): idx = hash.find(u("$"), 12) if idx == -1: raise uh.exc.MalformedHashError(cls, "unexpected end of rounds") rstr = hash[12:idx] try: rounds = int(rstr) except ValueError: raise uh.exc.MalformedHashError(cls, "bad rounds") if rstr != unicode(rounds): raise uh.exc.ZeroPaddedRoundsError(cls) if rounds == 0: # NOTE: not sure if this is forbidden by spec or not; # but allowing it would complicate things, # and it should never occur anyways. raise uh.exc.MalformedHashError(cls, "explicit zero rounds") salt_idx = idx+1 else: raise uh.exc.InvalidHashError(cls) # # 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) 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 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("$"): # $$-hash salt = hash[salt_idx:chk_idx-1] chk = hash[chk_idx+1:] bare_salt = False else: # $-hash salt = hash[salt_idx:chk_idx] chk = hash[chk_idx+1:] bare_salt = True return cls( rounds=rounds, salt=salt, checksum=chk, bare_salt=bare_salt, ) def to_string(self, withchk=True): ss = u('') if self.bare_salt else u('$') rounds = self.rounds if rounds > 0: hash = u("$md5,rounds=%d$%s%s") % (rounds, self.salt, ss) else: hash = u("$md5$%s%s") % (self.salt, ss) if withchk: chk = self.checksum if chk: 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. 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 = str_to_bascii(self.to_string(withchk=False)) return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii") #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/handlers/misc.py0000644000175000017500000002145612555044153021064 0ustar biscuitbiscuit00000000000000"""passlib.handlers.misc - misc generic handlers """ #============================================================================= # imports #============================================================================= # core import sys import logging; log = logging.getLogger(__name__) from warnings import warn # 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 #============================================================================= class unix_fallback(uh.StaticHandler): """This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`. This class does not implement a hash, but instead provides fallback behavior as found in /etc/shadow on most unix variants. If used, should be the last scheme in the context. * this class will positive identify all hash strings. * for security, newly encrypted passwords will hash to ``!``. * it rejects all passwords if the hash is NOT an empty string (``!`` or ``*`` are frequently used). * 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 its "wildcard" feature, and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead. """ name = "unix_fallback" context_kwds = ("enable_wildcard",) @classmethod def identify(cls, hash): 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 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 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.PasswordHash): """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. 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): 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, encoding=None): if hash is not None and not cls.identify(hash): raise uh.exc.InvalidHashError(cls) return cls.encrypt(secret, encoding) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/0000755000175000017500000000000012560246762017117 5ustar biscuitbiscuit00000000000000passlib-1.6.5/passlib/tests/__init__.py0000644000175000017500000000002412214647077021224 0ustar biscuitbiscuit00000000000000"""passlib tests""" passlib-1.6.5/passlib/tests/test_utils_handlers.py0000644000175000017500000007314312555044153023552 0ustar biscuitbiscuit00000000000000"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import re import hashlib from logging import getLogger import warnings # 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.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, patchAttr from passlib.utils.compat import u, PY3 # module log = getLogger(__name__) #============================================================================= # 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""" patchAttr = patchAttr #=================================================================== # StaticHandler #=================================================================== def test_00_static_handler(self): """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) # check default genconfig method self.assertIs(d1.genconfig(), 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. class d1(uh.StaticHandler): name = "d1" @classmethod def identify(cls, hash): if not hash or len(hash) != 40: return False try: int(hash, 16) except ValueError: return False return True @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 isinstance(hash, bytes): hash = hash.decode("ascii") if hash == u('a'): return cls(checksum=hash) else: raise ValueError # 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 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 checksum handling""" # setup helpers class d1(uh.GenericHandler): name = 'd1' checksum_size = 4 checksum_chars = u('xz') _stub_checksum = u('z')*4 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' 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')) # 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 = 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 mixin""" # setup helpers class d1(uh.HasRounds, uh.GenericHandler): name = 'd1' setting_kwds = ('rounds',) min_rounds = 1 max_rounds = 3 default_rounds = 2 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 d1.default_rounds = None self.assertRaises(TypeError, norm_rounds, use_defaults=True) def test_40_backends(self): """test GenericHandler + HasManyBackends mixin""" class d1(uh.HasManyBackends, uh.GenericHandler): name = 'd1' setting_kwds = () backends = ("a", "b") _has_backend_a = False _has_backend_b = False def _calc_checksum_a(self, secret): return 'a' def _calc_checksum_b(self, secret): return 'b' # 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 d1._has_backend_b = True # test lazy load obj = d1() self.assertEqual(obj._calc_checksum('s'), 'b') # test repeat load d1.set_backend('b') d1.set_backend('any') self.assertEqual(obj._calc_checksum('s'), 'b') # 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 d1._has_backend_a = True # test explicit self.assertTrue(d1.has_backend()) d1.set_backend('a') 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_norm_ident(self): """test GenericHandler + HasManyIdents""" # setup helpers class d1(uh.HasManyIdents, uh.GenericHandler): name = 'd1' setting_kwds = ('ident',) 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 # NOTE: +3 comes from int(math.log(.1,2)), # where 0.1 = 10% = default allowed variation in rounds self.patchAttr(hash.sha256_crypt, "default_rounds", 1 << (14 + 3)) self.assertEqual(hash.sha256_crypt.bitsize(), {'checksum': 258, 'rounds': 14, 'salt': 96}) # raw checksum self.patchAttr(hash.pbkdf2_sha1, "default_rounds", 1 << (13 + 3)) 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) class PrefixWrapperTest(TestCase): """test PrefixWrapper class""" def test_00_lazy_loading(self): """test PrefixWrapper lazy loading of handler""" d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}", lazy=True) # check base state self.assertEqual(d1._wrapped_name, "ldap_md5") self.assertIs(d1._wrapped_handler, None) # 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 with dummy_handler_in_registry("ldap_md5") as dummy: self.assertIs(d1.wrapped, ldap_md5) def test_01_active_loading(self): """test PrefixWrapper active loading of handler""" d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}") # 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 with dummy_handler_in_registry("ldap_md5") as dummy: self.assertIs(d1.wrapped, ldap_md5) def test_02_explicit(self): """test PrefixWrapper with explicitly specified handler""" d1 = uh.PrefixWrapper("d1", ldap_md5, "{XXX}", "{MD5}") # 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 with dummy_handler_in_registry("ldap_md5") as dummy: self.assertIs(d1.wrapped, ldap_md5) def test_10_wrapped_attributes(self): 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 self.assertIs(d1.genconfig(), None) # genhash self.assertEqual(d1.genhash("password", None), dph) self.assertEqual(d1.genhash("password", dph), dph) self.assertRaises(ValueError, d1.genhash, "password", lph) # encrypt self.assertEqual(d1.encrypt("password"), dph) # identify self.assertTrue(d1.identify(dph)) self.assertFalse(d1.identify(lph)) # verify self.assertRaises(ValueError, d1.verify, "password", lph) self.assertTrue(d1.verify("password", dph)) 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" checksum_chars = uh.LOWER_HEX_CHARS checksum_size = 40 def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") data = b("boblious") + secret return str_to_uascii(hashlib.sha1(data).hexdigest()) class SaltedHash(uh.HasSalt, uh.GenericHandler): """test algorithm with a salt""" name = "salted_test_hash" setting_kwds = ("salt",) min_salt_size = 2 max_salt_size = 4 checksum_size = 40 salt_chars = checksum_chars = uh.LOWER_HEX_CHARS _hash_regex = re.compile(u("^@salt[0-9a-f]{42,44}$")) @classmethod def from_string(cls, hash): if not cls.identify(hash): raise uh.exc.InvalidHashError(cls) if isinstance(hash, bytes): hash = hash.decode("ascii") return cls(salt=hash[5:-40], checksum=hash[-40:]) _stub_checksum = u('0') * 40 def to_string(self): hash = u("@salt%s%s") % (self.salt, self.checksum or self._stub_checksum) return uascii_to_str(hash) 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 str_to_uascii(hashlib.sha1(data).hexdigest()) #============================================================================= # test sample algorithms - really a self-test of HandlerCase #============================================================================= # 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 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(TypeError, UnsaltedHash.genconfig, rounds=1) class SaltedHashTest(HandlerCase): handler = SaltedHash known_correct_hashes = [ ("password", '@salt77d71f8fe74f314dac946766c1ac4a2a58365482c0'), (UPASS_TEMP, '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'), ] 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 #============================================================================= passlib-1.6.5/passlib/tests/sample1b.cfg0000644000175000017500000000037412214647077021310 0ustar biscuitbiscuit00000000000000[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 passlib-1.6.5/passlib/tests/test_apache.py0000644000175000017500000005576512555044153021765 0ustar biscuitbiscuit00000000000000"""tests for passlib.apache -- (c) Assurance Technologies 2008-2011""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import hashlib from logging import getLogger import os import time # site # pkg from passlib import apache from passlib.exc import MissingBackendError 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. atime = os.path.getatime(path) mtime = os.path.getmtime(path)-offset os.utime(path, (atime, mtime)) #============================================================================= # htpasswd #============================================================================= class HtpasswdFileTest(TestCase): """test HtpasswdFile class""" descriptionPrefix = "HtpasswdFile" # 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 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') sample_dup = b('user1:pass1\nuser1:pass2\n') # sample with bcrypt & sha256_crypt hashes sample_05 = b('user2:2CHkkwa2AtqGs\n' 'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n' 'user4:pass4\n' 'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n' 'user5:$2a$12$yktDxraxijBZ360orOyCOePFGhuis/umyPNJoL5EbsLk.s6SWdrRO\n' 'user6:$5$rounds=110000$cCRp/xUUGVgwR4aP$' 'p0.QKFS5qLNRqw1/47lXYiAcgIjJK.WjCO8nrEKuUK.\n') def test_00_constructor_autoload(self): """test constructor autoload""" # 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 new=True ht = apache.HtpasswdFile(path, new=True) self.assertEqual(ht.to_string(), b("")) self.assertEqual(ht.path, path) self.assertFalse(ht.mtime) # 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_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")) # should delete both entries self.assertTrue(ht.delete("user2")) 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_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) # 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_02_set_password_default_scheme(self): """test set_password() -- default_scheme""" def check(scheme): ht = apache.HtpasswdFile(default_scheme=scheme) ht.set_password("user1", "pass1") return ht.context.identify(ht.get_hash("user1")) # explicit scheme self.assertEqual(check("sha256_crypt"), "sha256_crypt") self.assertEqual(check("des_crypt"), "des_crypt") # unknown scheme self.assertRaises(KeyError, check, "xxx") # portable alias self.assertEqual(check("portable"), apache.portable_scheme) # default -- currently same as portable, will be host-specific under passlib 1.7. self.assertEqual(check(None), "apr_md5_crypt") def test_03_users(self): """test users()""" ht = apache.HtpasswdFile.from_string(self.sample_01) ht.set_password("user5", "pass5") ht.delete("user3") 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_05) self.assertRaises(TypeError, ht.check_password, 1, 'pass9') self.assertTrue(ht.check_password("user9","pass9") is None) # users 1..6 of sample_01 run through all the main hash formats, # to make sure they're recognized. for i in irange(1, 7): i = str(i) try: self.assertTrue(ht.check_password("user"+i, "pass"+i)) self.assertTrue(ht.check_password("user"+i, "pass9") is False) except MissingBackendError: if i == "5": # user5 uses bcrypt, which is apparently not available right now continue raise 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()""" # setup empty file path = self.mktemp() set_file(path, "") backdate_file_mtime(path, 5) ha = apache.HtpasswdFile(path, default_scheme="plaintext") self.assertEqual(ha.to_string(), b("")) # 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 set_file(path, self.sample_01) ha.load_if_changed() self.assertEqual(ha.to_string(), self.sample_01) # 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 hb = apache.HtpasswdFile() self.assertRaises(RuntimeError, hb.load) self.assertRaises(RuntimeError, hb.load_if_changed) # test load w/ dups and explicit path set_file(path, self.sample_dup) 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()""" # load from file path = self.mktemp() set_file(path, self.sample_01) ht = apache.HtpasswdFile(path) # 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(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' kwd""" # test bad encodings cause failure in constructor self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16") # 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_09_to_string(self): """test to_string""" # check with known sample ht = apache.HtpasswdFile.from_string(self.sample_01) self.assertEqual(ht.to_string(), self.sample_01) # test blank ht = apache.HtpasswdFile() self.assertEqual(ht.to_string(), b("")) 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""" descriptionPrefix = "HtdigestFile" # 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""" # 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 without autoload ht = apache.HtdigestFile(path, new=True) self.assertEqual(ht.to_string(), b("")) # 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) self.assertTrue(ht.delete("user1", "realm")) self.assertTrue(ht.delete("user2", "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") # 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.set_password("user2", "realm", "pass2x")) self.assertFalse(ht.set_password("user5", "realm", "pass5")) self.assertEqual(ht.to_string(), self.sample_03) # 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")) # TODO: test set_password autosave def test_03_users(self): """test users()""" ht = apache.HtdigestFile.from_string(self.sample_01) ht.set_password("user5", "realm", "pass5") ht.delete("user3", "realm") ht.set_password("user3", "realm", "pass3") self.assertEqual(ht.users("realm"), ["user2", "user4", "user1", "user5", "user3"]) 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.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")) # invalid user self.assertRaises(ValueError, ht.check_password, "user:", "realm", "pass") def test_05_load(self): """test load()""" # 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 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 set_file(path, self.sample_01) ha.load_if_changed() self.assertEqual(ha.to_string(), self.sample_01) # 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 hb = apache.HtdigestFile() self.assertRaises(RuntimeError, hb.load) 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()""" # load from file path = self.mktemp() set_file(path, self.sample_01) ht = apache.HtdigestFile(path) # 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 hb = apache.HtdigestFile() 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) self.assertEqual(ht.delete_realm("x"), 0) self.assertEqual(ht.realms(), ['realm']) self.assertEqual(ht.delete_realm("realm"), 4) self.assertEqual(ht.realms(), []) self.assertEqual(ht.to_string(), b("")) 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 self.assertRaises(ValueError, apache.HtdigestFile, encoding="utf-16") # 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) self.assertEqual(ht.to_string(), self.sample_01) # check blank ht = apache.HtdigestFile() self.assertEqual(ht.to_string(), b("")) 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 #============================================================================= passlib-1.6.5/passlib/tests/test_utils.py0000644000175000017500000010454012555044153021666 0ustar biscuitbiscuit00000000000000"""tests for passlib.util""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from binascii import hexlify, unhexlify import sys import random import 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 #============================================================================= class MiscTest(TestCase): """tests various parts of utils module""" # NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test 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 getrandbytes(rng, *a, **k) self.assertEqual(len(f(0)), 0) a = f(10) b = f(10) self.assertIsInstance(a, bytes) self.assertEqual(len(a), 10) self.assertEqual(len(b), 10) self.assertNotEqual(a, b) def test_getrandstr(self): """test getrandstr()""" from passlib.utils import getrandstr, rng def f(*a,**k): return getrandstr(rng, *a, **k) # count 0 self.assertEqual(f('abc',0), '') # count <0 self.assertRaises(ValueError, f, 'abc', -1) # letters 0 self.assertRaises(ValueError, f, '', 0) # letters 1 self.assertEqual(f('a',5), 'aaaaa') # 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')]) # 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')]) # 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(is_crypt_context(cc)) self.assertFalse(not is_crypt_context(cc)) def test_genseed(self): """test genseed()""" import random from passlib.utils import genseed rng = random.Random(genseed()) a = rng.randint(0, 100000) rng = random.Random(genseed()) b = rng.randint(0, 100000) self.assertNotEqual(a,b) rng.seed(genseed(rng)) 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_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< 200: # vulnerable to wraparound bug secret = secret[:200] if hash.startswith((IDENT_2B, IDENT_2Y)): hash = IDENT_2A + hash[4:] try: return bcrypt.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, IDENT_2B 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_2B, 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 other 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) # # test public methods against good & bad hashes # samples = self.known_incorrect_padding for pwd, bad, good in samples: # make sure genhash() corrects bad configs, leaves good unchanged 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_bcrypt_test, bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_test, bcrypt_builtin_test = \ _bcrypt_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"]) #============================================================================= # bcrypt #============================================================================= class _bcrypt_sha256_test(HandlerCase): "base for BCrypt-SHA256 test cases" handler = hash.bcrypt_sha256 reduce_default_rounds = True forbidden_characters = None fuzz_salts_need_bcrypt_repair = True fallback_os_crypt_handler = hash.bcrypt known_correct_hashes = [ # # custom test vectors # # empty ("", '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'), # ascii ("password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'), # unicode / utf8 (UPASS_TABLE, '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'), (UPASS_TABLE.encode("utf-8"), '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'), # test >72 chars is hashed correctly -- under bcrypt these hash the same. # NOTE: test_60_secret_size() handles this already, this is just for overkill :) (repeat_string("abc123",72), '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'), (repeat_string("abc123",72)+"qwr", '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'), (repeat_string("abc123",72)+"xyz", '$bcrypt-sha256$2a,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'), ] known_correct_configs =[ ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe', "password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'), ] known_malformed_hashes = [ # bad char in otherwise correct hash # \/ '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', # unrecognized bcrypt variant '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', # unsupported bcrypt variant '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', # rounds zero-padded '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu', # config string w/ $ added '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$', ] #=================================================================== # override some methods -- cloned from bcrypt #=================================================================== 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_sha256_test, self).setUp() warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*") def populate_settings(self, kwds): # builtin is still just way too slow. if self.backend == "builtin": kwds.setdefault("rounds", 4) super(_bcrypt_sha256_test, self).populate_settings(kwds) #=================================================================== # override ident tests for now #=================================================================== def test_30_HasManyIdents(self): raise self.skipTest("multiple idents not supported") def test_30_HasOneIdent(self): # forbidding ident keyword, we only support "2a" for now handler = self.handler handler(use_defaults=True) self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True) #=================================================================== # fuzz testing -- cloned from bcrypt #=================================================================== def fuzz_setting_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return randintgauss(5, 8, 6, 1) # create test cases for specific backends bcrypt_sha256_bcrypt_test, bcrypt_sha256_pybcrypt_test, bcrypt_sha256_bcryptor_test, bcrypt_sha256_os_crypt_test, bcrypt_sha256_builtin_test = \ _bcrypt_sha256_test.create_backend_cases(["bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin"]) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/test_handlers.py0000644000175000017500000023765312555044153022342 0ustar biscuitbiscuit00000000000000"""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 sys 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") and handler.name != "django_bcrypt_sha256": backend = handler.get_backend() name = "%s_%s_test" % (scheme, backend) else: name = "%s_test" % scheme try: return globals()[name] except KeyError: pass for suffix in ("handlers_django", "handlers_bcrypt"): modname = "passlib.tests.test_" + suffix __import__(modname) mod = sys.modules[modname] try: return getattr(mod, name) except AttributeError: pass raise KeyError("test case %r not found" % 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!' ] #============================================================================= # 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"]) #============================================================================= # 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... 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 #============================================================================= passlib-1.6.5/passlib/tests/backports.py0000644000175000017500000003255712555044153021467 0ustar biscuitbiscuit00000000000000"""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 #============================================================================= passlib-1.6.5/passlib/tests/test_handlers_django.py0000644000175000017500000003337312555044153023655 0ustar biscuitbiscuit00000000000000"""passlib.tests.test_handlers_django - 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 from passlib.tests.test_handlers import UPASS_WAV, UPASS_USD, UPASS_TABLE # module #============================================================================= # django #============================================================================= # standard string django uses UPASS_LETMEIN = u('l\xe8tmein') def vstr(version): return ".".join(str(e) for e in version) class _DjangoHelper(object): # NOTE: not testing against Django < 1.0 since it doesn't support # most of these hash formats. # flag that hash wasn't added until specified version min_django_version = () def fuzz_verifier_django(self): from passlib.tests.test_ext_django import DJANGO_VERSION # check_password() not added until 1.0 min_django_version = max(self.min_django_version, (1,0)) if DJANGO_VERSION < min_django_version: return None from django.contrib.auth.models import check_password def verify_django(secret, hash): """django/check_password""" if (1,4) <= DJANGO_VERSION < (1,6) and not secret: return "skip" if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): hash = hash.replace("$$2y$", "$$2a$") if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes): # e.g. unsalted_md5 on 1.5 and higher try to combine # salt + password before encoding to bytes, leading to ascii error. # this works around that issue. secret = secret.decode("utf-8") 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 DJANGO_VERSION # check_password() not added until 1.0 min_django_version = max(self.min_django_version, (1,0)) if DJANGO_VERSION < min_django_version: raise self.skipTest("Django >= %s not installed" % vstr(min_django_version)) from django.contrib.auth.models import check_password assert self.known_correct_hashes for secret, hash in self.iter_known_hashes(): if (1,4) <= DJANGO_VERSION < (1,6) and not secret: # django 1.4-1.5 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)) django_has_encoding_glitch = False def test_91_django_generation(self): """test against output of Django's make_password()""" from passlib.tests.test_ext_django import DJANGO_VERSION # make_password() not added until 1.4 min_django_version = max(self.min_django_version, (1,4)) if DJANGO_VERSION < min_django_version: raise self.skipTest("Django >= %s not installed" % vstr(min_django_version)) 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 if DJANGO_VERSION >= (1,5) and self.django_has_encoding_glitch and isinstance(secret, bytes): # e.g. unsalted_md5 on 1.5 and higher try to combine # salt + password before encoding to bytes, leading to ascii error. # this works around that issue. secret = secret.decode("utf-8") 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, "!"), ] known_alternate_hashes = [ # django 1.6 appends random alpnum string ("!9wa845vn7098ythaehasldkfj", "password", "!"), ] 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 django_has_encoding_glitch = True 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 django_has_encoding_glitch = True 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 min_django_version = (1,4) 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 min_django_version = (1,4) 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 min_django_version = (1,4) fuzz_salts_need_bcrypt_repair = 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) class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper): """test django_bcrypt_sha256""" handler = hash.django_bcrypt_sha256 min_django_version = (1,6) forbidden_characters = None fuzz_salts_need_bcrypt_repair = True known_correct_hashes = [ # # custom - generated via django 1.6 hasher # ('', 'bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu'), (UPASS_LETMEIN, 'bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC'), (UPASS_TABLE, 'bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u'), # test >72 chars is hashed correctly -- under bcrypt these hash the same. (repeat_string("abc123",72), 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni'), (repeat_string("abc123",72)+"qwr", 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG'), (repeat_string("abc123",72)+"xyz", 'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa'), ] known_malformed_hashers = [ # data in django salt field 'bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu', ] def test_30_HasManyIdents(self): raise self.skipTest("multiple idents not supported") def test_30_HasOneIdent(self): # forbidding ident keyword, django doesn't support configuring this handler = self.handler handler(use_defaults=True) self.assertRaises(TypeError, handler, ident="$2a$", use_defaults=True) # 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_sha256_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_sha256_test = skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available")(django_bcrypt_sha256_test) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/utils.py0000644000175000017500000026406212555721611020636 0ustar biscuitbiscuit00000000000000"""helpers for passlib unittests""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import logging; log = logging.getLogger(__name__) import re import os import sys import tempfile 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 # 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 'TEST_MODE', 'set_file', 'get_file', # unit testing 'TestCase', 'HandlerCase', ] #============================================================================= # 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 = False else: GAE = True 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. """ if min and _test_mode < _TEST_MODES.index(min): return False if max and _test_mode > _TEST_MODES.index(max): return False return True #============================================================================= # 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") 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 """ _orig = None 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): content = content.encode("utf-8") with open(path, "wb") as fh: fh.write(content) def get_file(path): """read file as bytes""" with open(path, "rb") as fh: return fh.read() 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 #============================================================================= def patchAttr(test, obj, attr, value): """monkeypatch object value, restoring original on cleanup""" try: orig = getattr(obj, attr) except AttributeError: def cleanup(): try: delattr(obj, attr) except AttributeError: pass test.addCleanup(cleanup) else: test.addCleanup(setattr, obj, attr, orig) setattr(obj, attr, value) class TestCase(_TestCase): """passlib-specific test case class 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 #--------------------------------------------------------------- # string prepended to all tests in TestCase descriptionPrefix = None def shortDescription(self): """wrap shortDescription() method to prepend descriptionPrefix""" desc = super(TestCase, self).shortDescription() prefix = self.descriptionPrefix if prefix: desc = "%s: %s" % (prefix, desc or str(self)) return desc #--------------------------------------------------------------- # 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): # make nose just proxy __unittest_skip__ return not cls.__unittest_skip__ # flag to skip *this* class __unittest_skip = True #--------------------------------------------------------------- # reset warning filters & registry before each test #--------------------------------------------------------------- # flag to reset all warning filters & ignore state resetWarningState = True def setUp(self): super(TestCase, self).setUp() self.setUpWarnings() def setUpWarnings(self): """helper to init warning filters before subclass setUp()""" if self.resetWarningState: ctx = reset_warnings() ctx.__enter__() self.addCleanup(ctx.__exit__) #--------------------------------------------------------------- # tweak message formatting so longMessage mode is only enabled # if msg ends with ":", and turn on longMessage by default. #--------------------------------------------------------------- longMessage = True 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 #--------------------------------------------------------------- # 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_re=None, filename=None, lineno=None, msg=None, ): """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 if message: self.assertEqual(str(warning), message, msg) if message_re: self.assertRegex(str(warning), message_re, msg) if category: self.assertIsInstance(warning, category, msg) # 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) In order to use this to test a handler, create a subclass will all the appropriate attributes filled as listed in the example below, and run the subclass via unittest. .. todo:: Document all of the options HandlerCase offers. .. note:: This is subclass of :class:`unittest.TestCase` (or :class:`unittest2.TestCase` if available). """ #=================================================================== # class attrs - should be filled in by subclass #=================================================================== #--------------------------------------------------------------- # handler setup #--------------------------------------------------------------- # handler class to test [required] handler = None # if set, run tests against specified backend backend = None #--------------------------------------------------------------- # test vectors #--------------------------------------------------------------- # list of (secret, hash) tuples which are known to be correct known_correct_hashes = [] # list of (config, secret, hash) tuples are known to be correct known_correct_configs = [] # 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 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) known_other_hashes = [ ('des_crypt', '6f8c114b58f2c'), ('md5_crypt', '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o.'), ('sha512_crypt', "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywW" "vt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"), ] # 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$') ] #--------------------------------------------------------------- # 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 #=================================================================== # 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 //= (1<= 1") # 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 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 if 'salt_size' not in cls.setting_kwds and \ (not mx_set or cls.min_salt_size < cls.max_salt_size): # 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,)) # 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") if any(c not in cls.salt_chars for c in cls.default_salt_chars): raise AssertionError("default_salt_chars must be subset of salt_chars: %r not in salt_chars" % (c,)) else: if not cls.default_salt_chars: raise AssertionError("default_salt_chars MUST be specified if salt_chars is empty") @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) # whether salt should be passed through bcrypt repair function fuzz_salts_need_bcrypt_repair = False def prepare_salt(self, salt): """prepare generated salt""" if self.fuzz_salts_need_bcrypt_repair: 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 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 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 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 if cls.rounds_cost not in rounds_cost_values: raise AssertionError("unknown rounds cost constant: %r" % (cls.rounds_cost,)) 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 self.assertTrue('ident' in cls.setting_kwds) # 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 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 if cls.ident_aliases: for alias, ident in iteritems(cls.ident_aliases): self.assertIsInstance(alias, unicode, "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,)) # 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 # create & hash string that's exactly sc+1 chars secret = repeat_string(base, sc+1) hash = self.do_encrypt(secret) # 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") else: # hash counts all characters; e.g. md5-crypt # 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") 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_62_secret_border(self): """test non-string passwords are rejected""" hash = self.get_sample_hash()[1] # 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) # 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 expect_os_crypt_failure(self, secret): """ check if we're expecting potential verify failure due to crypt.crypt() encoding limitation """ if PY3 and self.backend == "os_crypt" and isinstance(secret, bytes): try: secret.decode("utf-8") except UnicodeDecodeError: return True return False def test_70_hashes(self): """test known hashes""" # sanity check self.assertTrue(self.known_correct_hashes or self.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,)) # check if what we're about to do is expected to fail due to crypt.crypt() limitation. expect_os_crypt_failure = self.expect_os_crypt_failure(secret) try: # 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)) except MissingBackendError: if not expect_os_crypt_failure: raise # 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") 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: # 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,)) # 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: # 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: # 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. 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,)) 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") # # 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") # 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 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 = [] # 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 fuzz_verifier_crypt(self): """test results against OS crypt()""" handler = self.handler 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 fuzz_setting_rounds(self): handler = self.handler 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 fuzz_setting_salt_size(self): handler = self.handler 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 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 #--------------------------------------------------------------- # 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: size = randintgauss(50, 99, 70, 20) return getrandstr(rng, self.fuzz_password_alphabet, size) 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. * if crypt support is missing, inserts fake crypt support to simulate a working safe_crypt, to test passlib's codepath as fully as possible. * extra tests to verify non-conformant crypt implementations are handled correctly. * 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() # alternate handler to use for fake os_crypt, # e.g. bcrypt_sha256 uses bcrypt fallback_os_crypt_handler = None 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.fallback_os_crypt_handler or 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!") # create subclass of handler, which we swap to an alternate backend. # NOTE: not switching original class's backend, since classes like bcrypt # run some checks when backend is set, that can cause recursion error # when orig backend is restored. alt_handler = type('%s_%s_wrapper' % (handler.name, alt_backend), (handler,), {}) alt_handler._backend = None # ensure full backend load into subclass alt_handler.set_backend(alt_backend) import passlib.utils as mod def crypt_stub(secret, hash): # with temporary_backend(alt_handler, alt_backend): hash = alt_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(fallback=True): # 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)) #=================================================================== # 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") 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") 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)) #=================================================================== # 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"), ] 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 __enter__(self): # let parent class archive filter state ret = super(reset_warnings, self).__enter__() # 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): # 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 #============================================================================= passlib-1.6.5/passlib/tests/sample_config_1s.cfg0000644000175000017500000000035612214647077023015 0ustar biscuitbiscuit00000000000000[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 passlib-1.6.5/passlib/tests/test_hosts.py0000644000175000017500000000752212555044153021670 0ustar biscuitbiscuit00000000000000"""test passlib.hosts""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import logging; log = logging.getLogger(__name__) 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 #============================================================================= # 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 # # they mainly try to ensure no typos # or dynamic behavior foul-ups. def check_unix_disabled(self, ctx): for hash in [ "", "!", "*", "!$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0", ]: self.assertEqual(ctx.identify(hash), 'unix_disabled') self.assertFalse(ctx.verify('test', hash)) def test_linux_context(self): ctx = hosts.linux_context for hash in [ ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'), ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny' 'xDGgMlDcOsfaI17'), '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0', 'kAJJz.Rwp0A/I', ]: self.assertTrue(ctx.verify("test", hash)) self.check_unix_disabled(ctx) def test_bsd_contexts(self): for ctx in [ hosts.freebsd_context, hosts.openbsd_context, hosts.netbsd_context, ]: for hash in [ '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0', 'kAJJz.Rwp0A/I', ]: self.assertTrue(ctx.verify("test", hash)) 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_disabled(ctx) def test_host_context(self): ctx = getattr(hosts, "host_context", None) if not ctx: return self.skipTest("host_context not available on this platform") # validate schemes is non-empty, # 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_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_disabled(ctx) for scheme, hash in [ ("sha512_crypt", ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751')), ("sha256_crypt", ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny' 'xDGgMlDcOsfaI17')), ("md5_crypt", '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0'), ("des_crypt", 'kAJJz.Rwp0A/I'), ]: if scheme in schemes: self.assertTrue(ctx.verify("test", hash)) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/tox_support.py0000644000175000017500000000465112555044153022077 0ustar biscuitbiscuit00000000000000"""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 threadsafe: no handlers: - url: /.* script: dummy.py libraries: - name: django version: "latest" """ % runtime) def main(cmd, *args): return globals()["do_" + cmd](*args) if __name__ == "__main__": import sys sys.exit(main(*sys.argv[1:]) or 0) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/test_win32.py0000644000175000017500000000363512555044153021473 0ustar biscuitbiscuit00000000000000"""tests for passlib.win32 -- (c) Assurance Technologies 2003-2009""" #============================================================================= # imports #============================================================================= # core from binascii import hexlify import warnings # site # pkg from passlib.tests.utils import TestCase # 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")), ]: 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")), ]: result = raw_nthash(secret, hex=True) self.assertEqual(result, hash) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/test_ext_django.py0000644000175000017500000012410612560147067022654 0ustar biscuitbiscuit00000000000000"""test passlib.ext.django""" #============================================================================= # imports #============================================================================= # NOTE: double __future__ is workaround for py2.5.0 bug, # per https://bitbucket.org/ecollins/passlib/issues/58#comment-20589295 from __future__ import with_statement from __future__ import absolute_import # core import logging; log = logging.getLogger(__name__) import sys import warnings # site # pkg from passlib.apps import django10_context, django14_context, django16_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 # 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.conf import settings, LazySettings if not isinstance(settings, LazySettings): # 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 the unittests if has_django0: if settings._target is None: from django.conf import UserSettingsHolder, global_settings settings._target = UserSettingsHolder(global_settings) elif not settings.configured: settings.configure() #============================================================================= # support funcs #============================================================================= # flag for update_settings() to remove specified key entirely UNSET = object() def update_settings(**kwds): """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) else: delattr(settings, k) else: setattr(settings, k, v) if has_django: from django.contrib.auth.models import User 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, update_fields=None): # NOTE: ignoring update_fields for test purposes self.saved_passwords.append(self.password) 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 #============================================================================= sample_hashes = {} # override sample hashes used in test cases if DJANGO_VERSION >= (1,8): stock_config = django16_context.to_dict() stock_config.update( deprecated="auto", django_pbkdf2_sha1__default_rounds=20000, django_pbkdf2_sha256__default_rounds=20000, ) sample_hashes.update( django_pbkdf2_sha256=("not a password", "pbkdf2_sha256$20000$arJ31mmmlSmO$XNBTUKe4UCUGPeHTmXpYjaKmJaDGAsevd0LWvBtzP18="), ) elif DJANGO_VERSION >= (1,7): stock_config = django16_context.to_dict() stock_config.update( deprecated="auto", django_pbkdf2_sha1__default_rounds=15000, django_pbkdf2_sha256__default_rounds=15000, ) sample_hashes.update( django_pbkdf2_sha256=("not a password", "pbkdf2_sha256$15000$xb2YnidpItz1$uHvLChIjUDc5HVUfQnE6lDMbgkTAiSYknGCtjuX4AVo="), ) elif DJANGO_VERSION >= (1,6): stock_config = django16_context.to_dict() stock_config.update( deprecated="auto", django_pbkdf2_sha1__default_rounds=12000, django_pbkdf2_sha256__default_rounds=12000, ) sample_hashes.update( django_pbkdf2_sha256=("not a password", "pbkdf2_sha256$12000$rpUPFQOVetrY$cEcWG4DjjDpLrDyXnduM+XJUz25U63RcM3//xaFnBnw="), ) elif DJANGO_VERSION >= (1,4): stock_config = django14_context.to_dict() stock_config.update( deprecated="auto", django_pbkdf2_sha1__default_rounds=10000, django_pbkdf2_sha256__default_rounds=10000, ) elif DJANGO_VERSION >= (1,0): 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 _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): """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) 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() #=================================================================== # eoc #=================================================================== # XXX: rename to ExtensionFixture? class _ExtensionTest(TestCase, _ExtensionSupport): def setUp(self): super(_ExtensionTest, self).setUp() self.require_TEST_MODE("default") if not has_django: raise self.skipTest("Django not installed") # 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): """check that user object is set to 'unusable password' constant""" if DJANGO_VERSION >= (1,6): # 1.6 on adds a random(?) suffix self.assertTrue(user.password.startswith("!")) else: self.assertEqual(user.password, "!") if has_django1 or self.patched: self.assertFalse(user.has_usable_password()) self.assertEqual(user.pop_saved_passwords(), []) def assert_valid_password(self, user, hash=UNSET, saved=None): """check that user object has a usuable password hash. :param hash: optionally check it has this exact hash :param saved: check that mock commit history for user.password matches this list """ 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]) def test_config(self): """test hashing interface 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" has_hashers = False has_identify_hasher = False 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 or DJANGO_VERSION > (1,5): # identify_hasher() # django 1.4 -- not present # django 1.5 -- present (added in django ticket 18184) # passlib integration -- present even under 1.4 from django.contrib.auth.hashers import identify_hasher has_identify_hasher = True hash_hashers = True else: from django.contrib.auth.models import check_password #======================================================= # 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 (1,4) <= DJANGO_VERSION < (1,6): # NOTE: django 1.4-1.5 treat 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) # make_password() should reject empty passwords self.assertEqual(make_password(""), "!") # check_password() should never return True self.assertFalse(check_password("", hash)) 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: # sanity check via user.set_unusable_password() user = FakeUser() user.set_unusable_password() self.assert_unusable_password(user) # ensure User.set_password() sets unusable flag user = FakeUser() user.set_password(None) if DJANGO_VERSION < (1,2): # would set password to hash of "None" self.assert_valid_password(user) else: self.assert_unusable_password(user) # User.check_password() should always fail if DJANGO_VERSION < (1,2): self.assertTrue(user.check_password(None)) self.assertTrue(user.check_password('None')) self.assertFalse(user.check_password('')) self.assertFalse(user.check_password(PASS1)) self.assertFalse(user.check_password(WRONG1)) else: self.assertFalse(user.check_password(None)) self.assertFalse(user.check_password('None')) self.assertFalse(user.check_password('')) 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: if DJANGO_VERSION >= (1,6): self.assertTrue(make_password(None).startswith("!")) else: 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_identify_hasher: self.assertRaises(ValueError, identify_hasher, user.password) #======================================================= # hash=None #======================================================= # User.set_password() - n/a # 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: if DJANGO_VERSION < (1,2): self.assertTrue(user.has_usable_password()) else: self.assertFalse(user.has_usable_password()) # make_password() - n/a # check_password() - error if has_django14 or patched: self.assertFalse(check_password(PASS1, None)) else: self.assertRaises(AttributeError, check_password, PASS1, None) # identify_hasher() - error if has_identify_hasher: self.assertRaises(TypeError, identify_hasher, None) #======================================================= # empty & invalid hash values # NOTE: django 1.5 behavior change due to django ticket 18453 # NOTE: passlib integration tries to match current django version #======================================================= for hash in ("", # empty hash "$789$foo", # empty identifier ): # User.set_password() - n/a # User.check_password() # empty # ----- # django 1.3 and earlier -- blank hash returns False # django 1.4 -- blank threw error (fixed in 1.5) # django 1.5 -- blank hash returns False # # invalid # ------- # django 1.4 and earlier -- invalid hash threw error (fixed in 1.5) # django 1.5 -- invalid hash returns False user = FakeUser() user.password = hash if DJANGO_VERSION >= (1,5) or (not hash and DJANGO_VERSION < (1,4)): # returns False for hash self.assertFalse(user.check_password(PASS1)) else: # throws error for hash self.assertRaises(ValueError, user.check_password, PASS1) # verify hash wasn't changed/upgraded during check_password() call self.assertEqual(user.password, hash) self.assertEqual(user.pop_saved_passwords(), []) # User.has_usable_password() # passlib shim for django 0.x -- invalid/empty usable, to match 1.0-1.4 # django 1.0-1.4 -- invalid/empty usable (fixed in 1.5) # django 1.5 -- invalid/empty no longer usable if has_django1 or self.patched: if DJANGO_VERSION < (1,5): self.assertTrue(user.has_usable_password()) else: self.assertFalse(user.has_usable_password()) # make_password() - n/a # check_password() # django 1.4 and earlier -- invalid/empty hash threw error (fixed in 1.5) # django 1.5 -- invalid/empty hash now returns False if DJANGO_VERSION < (1,5): self.assertRaises(ValueError, check_password, PASS1, hash) else: self.assertFalse(check_password(PASS1, hash)) # identify_hasher() - throws error if has_identify_hasher: 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): # TODO: move this above get_handler_case(), # and omit MissingBackendError check. assert scheme in ["django_bcrypt", "django_bcrypt_sha256"], "%r scheme should always have active backend" % scheme continue try: secret, hash = sample_hashes[scheme] except KeyError: while True: secret, hash = testcase('setUp').get_sample_hash() if secret: # don't select blank passwords, especially under django 1.4/1.5 break other = 'dontletmein' # 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 # NOTE: needs_update kept separate in case we need to test rounds. needs_update = deprecated if needs_update: self.assertNotEqual(user.password, hash) 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_identify_hasher: 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", ) def setUp(self): super(ExtensionBehaviorTest, self).setUp() self.load_extension(PASSLIB_CONFIG=self.config) 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""" # check config="disabled" self.load_extension(PASSLIB_CONFIG="disabled", check=False) self.assert_unpatched() # check legacy config=None with self.assertWarningList("PASSLIB_CONFIG=None is deprecated"): self.load_extension(PASSLIB_CONFIG=None, check=False) self.assert_unpatched() # 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 # 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() # test disabled config self.load_extension(PASSLIB_CONFIG="disabled", check=False) self.assert_unpatched() def test_12_config_presets(self): """test PASSLIB_CONFIG=''""" # test django presets self.load_extension(PASSLIB_CONTEXT="django-default", check=False) if DJANGO_VERSION >= (1,6): ctx = django16_context elif DJANGO_VERSION >= (1,4): ctx = django14_context else: ctx = django10_context self.assert_patched(ctx) self.load_extension(PASSLIB_CONFIG="django-1.0", check=False) self.assert_patched(django10_context) self.load_extension(PASSLIB_CONFIG="django-1.4", check=False) self.assert_patched(django14_context) 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 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) # 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 #=================================================================== from passlib.context import CryptContext class ContextWithHook(CryptContext): """subclass which invokes update_hook(self) before major actions""" @staticmethod def update_hook(self): pass def encrypt(self, *args, **kwds): self.update_hook(self) return super(ContextWithHook, self).encrypt(*args, **kwds) def verify(self, *args, **kwds): self.update_hook(self) return super(ContextWithHook, self).verify(*args, **kwds) # hack up the some of the real django tests to run w/ extension loaded, # to ensure we mimic their behavior. # however, the django tests were moved out of the package, and into a source-only location # as of django 1.7. so we disable tests from that point on unless test-runner specifies test_hashers_mod = None hashers_skip_msg = None if TEST_MODE(max="quick"): hashers_skip_msg = "requires >= 'default' test mode" elif DJANGO_VERSION >= (1, 7): import os import sys source_path = os.environ.get("PASSLIB_TESTS_DJANGO_SOURCE_PATH") if source_path: if not os.path.exists(source_path): raise EnvironmentError("django source path not found: %r" % source_path) if not all(os.path.exists(os.path.join(source_path, name)) for name in ["django", "tests"]): raise EnvironmentError("invalid django source path: %r" % source_path) log.info("using django tests from source path: %r", source_path) tests_path = os.path.join(source_path, "tests") sys.path.insert(0, tests_path) from auth_tests import test_hashers as test_hashers_mod sys.path.remove(tests_path) else: hashers_skip_msg = "requires PASSLIB_TESTS_DJANGO_SOURCE_PATH to be set for django 1.7+" elif DJANGO_VERSION >= (1, 6): from django.contrib.auth.tests import test_hashers as test_hashers_mod elif DJANGO_VERSION >= (1, 4): from django.contrib.auth.tests import hashers as test_hashers_mod else: hashers_skip_msg = "requires django 1.4+ to be present" # hack up the some of the real django tests to run w/ extension loaded, # to ensure we mimic their behavior. if test_hashers_mod: from passlib.tests.utils import patchAttr class HashersTest(test_hashers_mod.TestUtilsHashPass, _ExtensionSupport): """run django's hasher unittests against passlib's extension and workalike implementations""" def setUp(self): # NOTE: omitted orig setup, want to install our extension, # and load hashers through it instead. self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) from passlib.ext.django.models import password_context # update test module to use our versions of some hasher funcs from django.contrib.auth import hashers for attr in ["make_password", "check_password", "identify_hasher", "get_hasher"]: patchAttr(self, test_hashers_mod, attr, getattr(hashers, attr)) # django 1.4 tests expect empty django_des_crypt salt field if DJANGO_VERSION >= (1,4): from passlib.hash import django_des_crypt patchAttr(self, django_des_crypt, "use_duplicate_salt", False) # hack: need password_context to keep up to date with hasher.iterations if DJANGO_VERSION >= (1,6): def update_hook(self): rounds = test_hashers_mod.get_hasher("pbkdf2_sha256").iterations self.update( django_pbkdf2_sha256__min_rounds=rounds, django_pbkdf2_sha256__default_rounds=rounds, django_pbkdf2_sha256__max_rounds=rounds, ) patchAttr(self, password_context, "__class__", ContextWithHook) patchAttr(self, password_context, "update_hook", update_hook) # omitting this test, since it depends on updated to django hasher settings test_pbkdf2_upgrade_new_hasher = lambda self: self.skipTest("omitted by passlib") def tearDown(self): self.unload_extension() super(HashersTest, self).tearDown() else: class HashersTest(TestCase): def test_external_django_hasher_tests(self): """external django hasher tests""" raise self.skipTest(hashers_skip_msg) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/test_registry.py0000644000175000017500000002236512555044153022402 0ustar biscuitbiscuit00000000000000"""tests for passlib.pwhash -- (c) Assurance Technologies 2003-2009""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import hashlib from logging import getLogger import os import time import warnings import sys # site # pkg from passlib import hash, registry, exc 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, catch_warnings # module log = getLogger(__name__) #============================================================================= # 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. #============================================================================= class dummy_0(uh.StaticHandler): name = "dummy_0" class alt_dummy_0(uh.StaticHandler): name = "dummy_0" dummy_x = 1 #============================================================================= # test registry #============================================================================= class RegistryTest(TestCase): descriptionPrefix = "passlib.registry" def setUp(self): super(RegistryTest, self).setUp() # backup registry state & restore it after test. locations = dict(registry._locations) handlers = dict(registry._handlers) def restore(): registry._locations.clear() registry._locations.update(locations) registry._handlers.clear() registry._handlers.update(handlers) self.addCleanup(restore) def test_hash_proxy(self): """test passlib.hash proxy object""" # check dir works dir(hash) # check repr works repr(hash) # 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. old = getattr(hash, "__loader__", None) test = object() hash.__loader__ = test self.assertIs(hash.__loader__, test) if old is None: del hash.__loader__ self.assertFalse(hasattr(hash, "__loader__")) else: hash.__loader__ = old self.assertIs(hash.__loader__, old) # 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 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._locations # check namespace is clear self.assertTrue('dummy_0' not in paths) self.assertFalse(hasattr(hash, 'dummy_0')) # 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)) self.assertIs(hash.dummy_0, dummy_0) self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True)) unload_handler_name('dummy_0') # 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 register_crypt_handler_path('dummy_x', __name__) self.assertRaises(TypeError, get_crypt_handler, 'dummy_x') # check lazy load w/ wrong name fails register_crypt_handler_path('alt_dummy_0', __name__) self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0") unload_handler_name("alt_dummy_0") # 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(): warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning) h = get_crypt_handler("dummy_bad") from passlib.tests import _test_bad_register as tbr self.assertIs(h, tbr.alt_dummy_bad) def test_register_crypt_handler(self): """test register_crypt_handler()""" self.assertRaises(TypeError, register_crypt_handler, {}) 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" class dummy_1b(uh.StaticHandler): name = "dummy_1" self.assertTrue('dummy_1' not in list_crypt_handlers()) register_crypt_handler(dummy_1) register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) self.assertRaises(KeyError, register_crypt_handler, dummy_1b) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) register_crypt_handler(dummy_1b, force=True) self.assertIs(get_crypt_handler("dummy_1"), dummy_1b) self.assertTrue('dummy_1' in list_crypt_handlers()) def test_get_crypt_handler(self): """test get_crypt_handler()""" 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) # 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) unload_handler_name("_fake") def test_handlers(self): """verify we have tests for all builtin handlers""" from passlib.registry import list_crypt_handlers from passlib.tests.test_handlers import get_handler_case for name in list_crypt_handlers(): # skip some wrappers that don't need independant testing if name.startswith("ldap_") and name[5:] in list_crypt_handlers(): continue if name in ["roundup_plaintext"]: continue # check the remaining ones all have a handler try: self.assertTrue(get_handler_case(name)) except exc.MissingBackendError: if name in ["bcrypt", "bcrypt_sha256"]: # expected to fail on some setups continue raise #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/tests/sample1c.cfg0000644000175000017500000000075212214647077021311 0ustar biscuitbiscuit00000000000000ÿþ[mypolicy] 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 passlib-1.6.5/passlib/tests/__main__.py0000644000175000017500000000012212214647077021204 0ustar biscuitbiscuit00000000000000import os from nose import run run( defaultTest=os.path.dirname(__file__), ) passlib-1.6.5/passlib/tests/test_context_deprecated.py0000644000175000017500000007154312555044153024400 0ustar biscuitbiscuit00000000000000"""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 #============================================================================= passlib-1.6.5/passlib/tests/test_context.py0000644000175000017500000017634312555044153022224 0ustar biscuitbiscuit00000000000000"""tests for passlib.context""" #============================================================================= # imports #============================================================================= # core from __future__ import with_statement from passlib.utils.compat import PY3 if PY3: from configparser import NoSectionError else: from ConfigParser import NoSectionError import hashlib import logging; log = logging.getLogger(__name__) import re import os import time import warnings import sys # site # pkg from passlib import hash 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, 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 CryptContextTest(TestCase): descriptionPrefix = "CryptContext" # TODO: these unittests could really use a good cleanup # and reorganizing, to ensure they're getting everything. #=================================================================== # 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_1_dict = dict( schemes = sample_1_schemes, default = "md5_crypt", 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_1_resolved_dict = merge_dicts(sample_1_dict, schemes = sample_1_handlers) sample_1_unnormalized = u("""\ [passlib] 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_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, # using this to test partial replacement of existing options sha512_crypt__min_rounds=45000, ) sample_2_unicode = """\ [passlib] bsdi_crypt__min_rounds = 29000 bsdi_crypt__max_rounds = 35000 bsdi_crypt__default_rounds = 31000 sha512_crypt__min_rounds = 45000 """ # sample 2 overlayed on top of sample 1 sample_12_dict = merge_dicts(sample_1_dict, sample_2_dict) #--------------------------------------------------------------- # 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 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, ) #=================================================================== # 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) # test unicode scheme names (issue 54) ctx = CryptContext(schemes=[u("sha256_crypt")]) self.assertEqual(ctx.schemes(), ("sha256_crypt",)) 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 its 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() # 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]) # names must be unique class dummy_1(uh.StaticHandler): name = 'dummy_1' self.assertRaises(KeyError, CryptContext, schemes=[dummy_1, dummy_1]) # 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']) # deprecating ALL schemes should cause ValueError self.assertRaises(ValueError, CryptContext, schemes=['des_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",)) # # sha512_crypt # self.assertEqual(options(cc4, "sha512_crypt"), dict( deprecated=True, vary_rounds=0.1, # inherited from all__ max_rounds=20000, )) self.assertEqual(options(cc4, "sha512_crypt", "user"), dict( deprecated=True, # unconfigured category inherits from default vary_rounds=0.1, max_rounds=20000, )) 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 )) # # des_crypt # self.assertEqual(options(cc4, "des_crypt"), dict( deprecated=True, vary_rounds=0.1, )) self.assertEqual(options(cc4, "des_crypt", "user"), dict( deprecated=True, # unconfigured category inherits from default vary_rounds=0.1, )) self.assertEqual(options(cc4, "des_crypt", "admin"), dict( deprecated=True, # unchanged though overidden vary_rounds=0.05, # global overridden per-cateogry )) # # bsdi_crypt # self.assertEqual(options(cc4, "bsdi_crypt"), dict( vary_rounds=0.2, # overridden from all__vary_rounds )) self.assertEqual(options(cc4, "bsdi_crypt", "user"), dict( vary_rounds=0.2, # unconfigured category inherits from default )) self.assertEqual(options(cc4, "bsdi_crypt", "admin"), dict( vary_rounds=0.3, deprecated=True, # deprecation set per-category )) def test_34_to_dict(self): """test to_dict() method""" # 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_35_to_string(self): """test to_string() method""" # 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.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, {}), ((), {}), ] 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) # 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)) # test default h = cc.encrypt("test") self.assertEqual(cc.identify(h), "md5_crypt") # test genhash h = cc.genhash('secret', cc.genconfig()) self.assertEqual(cc.identify(h), 'md5_crypt') h = cc.genhash('secret', cc.genconfig(), scheme='md5_crypt') self.assertEqual(cc.identify(h), 'md5_crypt') self.assertRaises(ValueError, cc.genhash, 'secret', cc.genconfig(), scheme="des_crypt") 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")) # override scheme & custom settings self.assertEqual( cc.genconfig(scheme="phpass", salt='.'*8, rounds=8, ident='P'), '$P$6........', ) #-------------------------------------------------------------- # 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="phpass", salt='.'*8), '$H$5........De04R5Egz0aq8Tf.1eVhY/', ) self.assertEqual( cc.encrypt("password", scheme="phpass", salt='.'*8, ident="P"), '$P$5........De04R5Egz0aq8Tf.1eVhY/', ) # NOTE: more thorough job of rounds limits done below. # min rounds with self.assertWarningList(PasslibConfigWarning): self.assertEqual( cc.encrypt("password", rounds=1999, salt="nacl"), '$5$rounds=2000$nacl$9/lTZ5nrfPuz8vphznnmHuDGFuvjSNvOEDsGmGfsS97', ) with self.assertWarningList([]): self.assertEqual( cc.encrypt("password", rounds=2001, salt="nacl"), '$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31' ) # NOTE: max rounds, etc tested in genconfig() # make default > max throws error if attempted self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=4000) #-------------------------------------------------------------- # border cases #-------------------------------------------------------------- # rejects non-string secrets cc = CryptContext(["des_crypt"]) for secret, kwds in self.nonstring_vectors: self.assertRaises(TypeError, cc.encrypt, secret, **kwds) # 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) def test_44_identify(self): """test identify() border cases""" handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] cc = CryptContext(handlers, bsdi_crypt__default_rounds=5) # check unknown hash self.assertEqual(cc.identify('$9$232323123$1287319827'), None) self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', 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) # 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, bsdi_crypt__default_rounds=5) h = hash.md5_crypt.encrypt("test") # check base verify self.assertTrue(cc.verify("test", h)) self.assertTrue(not cc.verify("notest", h)) # 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 self.assertRaises(ValueError, cc.verify, 'test', h, scheme='bsdi_crypt') #-------------------------------------------------------------- # 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$") # 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""" 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""" name = "timed_hash" delay = 0 @classmethod def identify(cls, hash): return True 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 = tick() result = func(*args, **kwds) return tick()-start, result # verify genhash delay works TimedHash.delay = min_delay elapsed, result = timecall(TimedHash.genhash, 'stub', None) self.assertEqual(result, 'stubx') self.assertAlmostEqual(elapsed, min_delay, delta=delta) # 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) # 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 self.assertWarningList(".*verify exceeded min_verify_time"): elapsed, result = timecall(cc.verify, "blob", "stubx") self.assertFalse(result) self.assertAlmostEqual(elapsed, max_delay, delta=delta) # reject values < 0 self.assertRaises(ValueError, CryptContext, min_verify_time=-1) def test_61_autodeprecate(self): """test deprecated='auto' is handled correctly""" def getstate(ctx, category=None): return [ctx._is_deprecated_scheme(scheme, category) for scheme in ctx.schemes()] # 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 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.assertTrue(new_hash and new_hash != even_hash) #=================================================================== # eoc #=================================================================== #============================================================================= # LazyCryptContext #============================================================================= class dummy_2(uh.StaticHandler): name = "dummy_2" class LazyCryptContextTest(TestCase): descriptionPrefix = "LazyCryptContext" def setUp(self): # make sure this isn't registered before OR after 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") cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) self.assertFalse(has_crypt_handler("dummy_2", True)) self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt")) self.assertTrue(cc._is_deprecated_scheme("des_crypt")) self.assertTrue(has_crypt_handler("dummy_2", True)) def test_callable_constructor(self): self.assertFalse(has_crypt_handler("dummy_2")) register_crypt_handler_path("dummy_2", "passlib.tests.test_context") def onload(flag=False): self.assertTrue(flag) return dict(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"]) cc = LazyCryptContext(onload=onload, flag=True) self.assertFalse(has_crypt_handler("dummy_2", True)) 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 #============================================================================= passlib-1.6.5/passlib/tests/test_utils_crypto.py0000644000175000017500000005736612555044153023303 0ustar biscuitbiscuit00000000000000"""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 # 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 #============================================================================= passlib-1.6.5/passlib/hash.py0000644000175000017500000000174712555044153017255 0ustar biscuitbiscuit00000000000000"""passlib.hash - proxy object mapping hash scheme names -> handlers Note ==== This module does not actually contain any hashes. This file is a stub that replaces itself with a proxy object. 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() #============================================================================= # import proxy object and replace this module #============================================================================= from passlib.registry import _proxy import sys sys.modules[__name__] = _proxy #============================================================================= # eoc #============================================================================= passlib-1.6.5/passlib/registry.py0000644000175000017500000003745512555044153020207 0ustar biscuitbiscuit00000000000000"""passlib.registry - registry for password hash handlers""" #============================================================================= # imports #============================================================================= # core import re import logging; log = logging.getLogger(__name__) from warnings import warn # pkg from passlib.exc import ExpectedTypeError, PasslibWarning from passlib.utils import is_crypt_handler from passlib.utils.compat import native_string_types # local __all__ = [ "register_crypt_handler_path", "register_crypt_handler", "get_crypt_handler", "list_crypt_handlers", ] #============================================================================= # 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 the requested password hash algorithm from wherever it has been stored. it acts as a thin wrapper around :func:`passlib.registry.get_crypt_handler`. """ __name__ = "passlib.hash" __package__ = None def __getattr__(self, attr): if attr.startswith("_"): raise AttributeError("missing attribute: %r" % (attr,)) handler = get_crypt_handler(attr, None) if handler: return handler else: raise AttributeError("unknown password hash: %r" % (attr,)) def __setattr__(self, attr, value): if attr.startswith("_"): # writing to private attributes should behave normally. # (required so GAE can write to the __loader__ attribute). object.__setattr__(self, attr, value) else: # 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): # 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(_locations) return sorted(attrs) # create single instance - available publically as 'passlib.hash' _proxy = _PasslibRegistryProxy() #============================================================================= # 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 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", bcrypt_sha256 = "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_bcrypt_sha256 = "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. custom hashes may be registered via :func:`register_crypt_handler`, or they may be registered by this function, which will delay actually importing and loading the handler until a call to :func:`get_crypt_handler` is made for the specified name. :arg name: name of handler :arg path: module import path the specified module path should contain a password hash handler called :samp:`{name}`, or the path may contain a colon, specifying the module and module attribute to use. for example, the following would cause ``get_handler("myhash")`` to look for a class named ``myhash`` within the ``myapp.helpers`` module:: >>> from passlib.registry import registry_crypt_handler_path >>> registry_crypt_handler_path("myhash", "myapp.helpers") ...while this form would cause ``get_handler("myhash")`` to look for a class name ``MyHash`` within the ``myapp.helpers`` module:: >>> from passlib.registry import registry_crypt_handler_path >>> registry_crypt_handler_path("myhash", "myapp.helpers:MyHash") """ # validate name _validate_handler_name(name) # validate path if path.startswith("."): raise ValueError("path cannot start with '.'") if ':' in path: 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, _attr=None): """register password hash handler. this method immediately registers a handler with the internal passlib registry, so that it will be returned by :func:`get_crypt_handler` when requested. :arg handler: the password hash handler to register :param force: force override of existing handler (defaults to False) :param _attr: [internal kwd] if specified, ensures ``handler.name`` matches this value, or raises :exc:`ValueError`. :raises TypeError: if the specified object does not appear to be a valid handler. :raises ValueError: if the specified object's name (or other required attributes) contain invalid values. :raises KeyError: if a (different) handler was already registered with the same name, and ``force=True`` was not specified. """ # validate handler if not is_crypt_handler(handler): 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 (%r != %r)" % (_attr, name)) # check for existing handler other = _handlers.get(name) if other: if other is handler: 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("another %r handler has already been registered: %r" % (name, other)) # register handler _handlers[name] = handler log.debug("registered %r handler: %r", name, handler) def get_crypt_handler(name, default=_UNSET): """return handler for specified password hash scheme. this method looks up a handler for the specified scheme. if the handler is not already loaded, it checks if the location is known, and loads it first. :arg name: name of handler to return :param default: optional default value to return if no handler with specified name is found. :raises KeyError: if no handler matching that name is found, and no default specified, a KeyError will be raised. :returns: handler attached to name, or default value (if specified). """ # 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 is already loaded try: return _handlers[name] except KeyError: pass # normalize name (and if changed, check dict again) assert isinstance(name, native_string_types), "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), PasslibWarning, stacklevel=2) name = alt # 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) # 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(), # (this is discouraged due to its magical implicitness) handler = _handlers.get(name) if handler: # 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 handler = getattr(mod, modattr) register_crypt_handler(handler, _attr=name) return handler # fail! if default is _UNSET: raise KeyError("no crypt handler found for algorithm: %r" % (name,)) else: return default def list_crypt_handlers(loaded_only=False): """return sorted list of all known crypt handler names. :param loaded_only: if ``True``, only returns names of handlers which have actually been loaded. :returns: list of names of all known handlers """ names = set(_handlers) if not loaded_only: 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... def _has_crypt_handler(name, loaded_only=False): """check if handler name is known. this is only useful for two cases: * quickly checking if handler has already been loaded * checking if handler exists, without actually loading it :arg name: name of handler :param loaded_only: if ``True``, returns False if handler exists but hasn't been loaded """ 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. .. warning:: this is an internal function, used only by the unittests. if loaded handler is found with specified name, it's removed. if path to lazy load handler is found, it's removed. missing names are a noop. :arg name: name of handler to unload :param locations: if False, won't purge registered handler locations (default True) """ if name in _handlers: del _handlers[name] if locations and name in _locations: del _locations[name] #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/apps.py0000644000175000017500000001507012555044153017267 0ustar biscuitbiscuit00000000000000"""passlib.apps""" #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) from itertools import chain # site # pkg from passlib import hash from passlib.context import LazyCryptContext from passlib.utils import sys_bits # local __all__ = [ 'custom_app_context', 'django_context', 'ldap_context', 'ldap_nocrypt_context', 'mysql_context', 'mysql4_context', 'mysql3_context', 'phpass_context', 'phpbb3_context', 'postgres_context', ] #============================================================================= # 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 schemes=["sha512_crypt", "sha256_crypt"], # 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__min_rounds = 535000, sha256_crypt__min_rounds = 535000, # if the admin user category is selected, make a much stronger hash, admin__sha512_crypt__min_rounds = 1024000, admin__sha256_crypt__min_rounds = 1024000, ) #============================================================================= # 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"], ) _django14_schemes = ["django_pbkdf2_sha256", "django_pbkdf2_sha1", "django_bcrypt"] + _django10_schemes django14_context = LazyCryptContext( schemes=_django14_schemes, deprecated=_django10_schemes, ) _django16_schemes = _django14_schemes[:] _django16_schemes.insert(1, "django_bcrypt_sha256") django16_context = LazyCryptContext( schemes=_django16_schemes, deprecated=_django10_schemes, ) # this will always point to latest version django_context = django16_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 ldap_nocrypt_context = LazyCryptContext(std_ldap_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) def _iter_ldap_schemes(): """helper which iterates over supported std ldap schemes""" 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 ##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 #============================================================================= mysql3_context = LazyCryptContext(["mysql323"]) mysql4_context = LazyCryptContext(["mysql41", "mysql323"], deprecated="mysql323") mysql_context = mysql4_context # tracks latest mysql version supported #============================================================================= # postgres #============================================================================= postgres_context = LazyCryptContext(["postgres_md5"]) #============================================================================= # phpass & variants #============================================================================= def _create_phpass_policy(**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"], onload=_create_phpass_policy, ) phpbb3_context = LazyCryptContext(["phpass"], phpass__ident="H") # TODO: support the drupal phpass variants (see phpass homepage) #============================================================================= # 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+ roundup_context = roundup15_context = LazyCryptContext( schemes=_std_roundup_schemes + [ "ldap_pbkdf2_sha1" ], deprecated=_std_roundup_schemes, default = "ldap_pbkdf2_sha1", ldap_pbkdf2_sha1__default_rounds = 10000, ) #============================================================================= # eof #============================================================================= passlib-1.6.5/passlib/apache.py0000644000175000017500000011602512555044153017547 0ustar biscuitbiscuit00000000000000"""passlib.apache - apache password support""" # XXX: relocate this to passlib.ext.apache? #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from hashlib import md5 import logging; log = logging.getLogger(__name__) import os import sys from warnings import warn # site # pkg from passlib.context import CryptContext 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', ] #============================================================================= # 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. 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): """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. :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_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(path) return self #=================================================================== # init #=================================================================== def __init__(self, path=None, new=False, autoload=True, autosave=False, encoding="utf-8", return_unicode=PY3, ): # 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 # 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() else: self._records = OrderedDict() 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) @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, path=None, force=True): """Load state from local file. If no path is specified, attempts to load from ``self.path``. :type path: str :arg path: local file to load from :type force: bool :param force: if ``force=False``, only load from ``self.path`` if file has changed since last load. .. deprecated:: 1.6 This keyword will be removed in Passlib 1.8; Applications should use :meth:`load_if_changed` instead. """ 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): """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(self, 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__) 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""" return (self._render_record(key,value) for key,value in iteritems(self._records)) def _render_record(self, 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). :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). :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 value # 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 #============================================================================= #: default CryptContext used by HtpasswdFile # TODO: update this to support everything in host_context (where available), # and note in the documentation that the default is no longer guaranteed to be portable # across platforms. # c.f. http://httpd.apache.org/docs/2.2/programs/htpasswd.html htpasswd_context = CryptContext([ # man page notes supported everywhere; is default on Windows, Netware, TPF "apr_md5_crypt", # [added in passlib 1.6.3] # apache requires host crypt() support; but can generate natively # (as of https://bz.apache.org/bugzilla/show_bug.cgi?id=49288) "bcrypt", # [added in passlib 1.6.3] # apache requires host crypt() support; and can't generate natively "sha256_crypt", "sha512_crypt", # man page notes apache does NOT support this on Windows, Netware, TPF "des_crypt", # man page notes intended only for transitioning htpasswd <-> ldap "ldap_sha1", # man page notes apache ONLY supports this on Windows, Netware, TPF "plaintext" ]) #: scheme that will be used when 'portable' is requested. portable_scheme = "apr_md5_crypt" class HtpasswdFile(_CommonFile): """class for reading & writing Htpasswd files. The class constructor accepts the following arguments: :type path: filepath :param path: Specifies path to htpasswd file, use to implicitly load from and save to. This class has two modes of operation: 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 :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 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. May be any of ``"bcrypt"``, ``"sha256_crypt"``, ``"apr_md5_crypt"``, ``"des_crypt"``, ``"ldap_sha1"``, ``"plaintext"``. It defaults to ``"apr_md5_crypt"``. .. note:: Some hashes are only supported by apache / htpasswd on certain operating systems (e.g. bcrypt on BSD, sha256_crypt on linux). To get the strongest hash that's still portable, applications can specify ``default_scheme="portable"``. .. versionadded:: 1.6 This keyword was previously named ``default``. That alias has been deprecated, and will be removed in Passlib 1.8. .. versionchanged:: 1.6.3 Added support for ``"bcrypt"``, ``"sha256_crypt"``, and ``"portable"``. :type context: :class:`~passlib.context.CryptContext` :param context: :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 option may be used to add support for non-standard hash formats to an htpasswd file. However, the resulting file will probably not be usable 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:: check_password .. automethod:: get_hash Modification ================ .. automethod:: set_password .. automethod:: delete Alternate Constructors ====================== .. automethod:: from_string 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. """ #=================================================================== # 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: if default_scheme == "portable": default_scheme = portable_scheme context = context.copy(default=default_scheme) self.context = context super(HtpasswdFile, self).__init__(path, **kwds) 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_record(self, user, hash): return render_bytes("%s:%s\n", user, hash) #=================================================================== # public methods #=================================================================== def users(self): """Return list of all users in database""" return [self._decode_field(user) for user in self._records] ##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() 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._encode_user(user) hash = self.context.encrypt(password) 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. :returns: * ``True`` if user deleted. * ``False`` if user not found. """ try: del self._records[self._encode_user(user)] except KeyError: return False self._autosave() return True def check_password(self, user, password): """Verify password for specified user. :returns: * ``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._encode_user(user) hash = self._records.get(user) if hash is None: return None 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) #=================================================================== # eoc #=================================================================== #============================================================================= # htdigest editing #============================================================================= class HtdigestFile(_CommonFile): """class for reading & writing Htdigest files. The class constructor accepts the following arguments: :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: 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 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 Inspection ========== .. automethod:: realms .. automethod:: users .. automethod:: check_password(user[, realm], password) .. automethod:: get_hash Modification ============ .. automethod:: set_password(user[, realm], password) .. automethod:: delete .. automethod:: delete_realm Alternate Constructors ====================== .. automethod:: from_string Attributes ========== .. attribute:: default_realm 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. .. 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 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 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): """set password for user""" return self.set_password(user, realm, password) # 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. """ 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) # XXX: rename to something more explicit, like delete_user()? def delete(self, user, realm=None): """Delete user's entry for specified realm. if realm is not specified, uses ``self.default_realm``. :returns: * ``True`` if user deleted, * ``False`` if user not found in 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. if realm is not specified, uses ``self.default_realm``. :returns: number of users deleted (0 if realm not found) """ realm = self._encode_realm(realm) records = self._records keys = [key for key in records if key[1] == realm] for key in keys: del records[key] self._autosave() return len(keys) def check_password(self, user, realm=None, password=_UNSET): """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 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 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 #============================================================================= passlib-1.6.5/passlib/win32.py0000644000175000017500000000504512555044153017267 0ustar biscuitbiscuit00000000000000"""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. in particular, they have no identifying marks, both being 32 bytes of binary data. thus, they can't be easily identified in a context with other hashes, so a CryptHandler hasn't been defined for them. this module provided two functions to aid in any use-cases which exist. .. warning:: these functions should not be used for new code unless an existing system requires them, they are both known broken, and are beyond insecure on their own. .. autofunction:: raw_lmhash .. autofunction:: raw_nthash See also :mod:`passlib.hash.nthash`. """ 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.compat import b, unicode from passlib.utils.des import des_encrypt_block from passlib.hash import nthash # local __all__ = [ "nthash", "raw_lmhash", "raw_nthash", ] #============================================================================= # 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 its 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 #============================================================================= passlib-1.6.5/setup.cfg0000644000175000017500000000022612560246762016141 0ustar biscuitbiscuit00000000000000[upload] sign = true [upload_docs] upload_dir = build/sphinx/html [wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0