passlib-1.7.1/0000755000175000017500000000000013043774617014317 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/0000755000175000017500000000000013043774617015247 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/index.rst0000644000175000017500000000566313043655543017116 0ustar biscuitbiscuit00000000000000.. image:: _static/masthead.png :align: center :class: show-for-small .. rst-class:: float-right .. seealso:: :ref:`What's new in Passlib 1.7 ` ========================================== Passlib |release| documentation ========================================== .. only:: devcopy .. warning:: This is the documentation for a development version of Passlib. For documentation of the latest stable version, see ``_. .. only:: pypi .. warning:: The official Passlib documentation have moved to ``_. Documentation at this location is still being maintained, but will be updated much less frequently. 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:`PBKDF2-SHA256 ` algorithm:: >>> # import the hash algorithm >>> from passlib.hash import pbkdf2_sha256 >>> # generate new salt, and hash a password >>> hash = pbkdf2_sha256.hash("toomanysecrets") >>> hash '$pbkdf2-sha256$29000$N2YMIWQsBWBMae09x1jrPQ$1t8iyB2A.WF/Z5JZv.lfCIhXXN33N23OSgQYThBYRfk' >>> # verifying the password >>> pbkdf2_sha256.verify("toomanysecrets", hash) True >>> pbkdf2_sha256.verify("joshua", hash) False .. rst-class:: toc-always-open Getting Started =============== This documentation is organized into two main parts: a narrative walkthrough of Passlib, and a top-down API reference. :doc:`narr/index` New users in particular will want to visit the walkthrough, as it provides introductory documentation including installation requirements, an overview of what passlib provides, and a guide for getting started quickly. :doc:`lib/index` The API reference contains a top-down reference of the :mod:`!passlib` package. :doc:`other` This section contains additional things that don't fit anywhere else, including an :doc:`FAQ ` and a complete :doc:`changelog `. Online Resources ================ .. table:: :class: fullwidth :column-alignment: lr =================== =================================================== Latest Docs: ``_ Project Home: ``_ News & Discussion: ``_ Downloads @ PyPI: ``_ =================== =================================================== passlib-1.7.1/docs/history.rst0000644000175000017500000000014413015210360017454 0ustar biscuitbiscuit00000000000000:orphan: .. redirect stub .. seealso:: This page has been moved to :doc:`history/index`. passlib-1.7.1/docs/copyright.rst0000644000175000017500000000003012214647122017767 0ustar biscuitbiscuit00000000000000.. include:: ../LICENSE passlib-1.7.1/docs/_fragments/0000755000175000017500000000000013043774617017374 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/_fragments/trivial_hash_warning.rst0000644000175000017500000000040713015205366024316 0ustar biscuitbiscuit00000000000000.. rst-class:: block-title .. danger:: **This algorithm is dangerously insecure by modern standards.** It is trivially broken, and should not be used if at all possible. For new code, see the list of :ref:`recommended hashes `. passlib-1.7.1/docs/_fragments/insecure_hash_warning.rst0000644000175000017500000000050013015205366024453 0ustar biscuitbiscuit00000000000000.. rst-class:: block-title .. danger:: **This algorithm is not considered secure by modern standards.** It should only be used when verifying existing hashes, or when interacting with applications that require this format. For new code, see the list of :ref:`recommended hashes `. passlib-1.7.1/docs/_fragments/asa_verify_callout.rst0000644000175000017500000000113413043701620023760 0ustar biscuitbiscuit00000000000000.. rst-class:: float-right without-title .. todo:: **Caveat Emptor** Passlib's implementations of :class:`cisco_pix` and :class:`cisco_asa` both need verification. For those with access to Cisco PIX and ASA systems, verifying Passlib's reference vectors would be a great help (see :issue:`51`). In the mean time, there are no guarantees that passlib correctly replicates the official implementation. .. versionchanged:: 1.7.1 A number of :ref:`bugs ` were fixed after expanding the reference vectors, and testing against an ASA 9.6 system. passlib-1.7.1/docs/conf.py0000644000175000017500000002741013043720714016537 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)) # ignore warnings when documenting deprecated passlib methods import warnings warnings.filterwarnings("ignore", category=DeprecationWarning, module="passlib[.].*") #============================================================================= # imports #============================================================================= import datetime # 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 #============================================================================= # General configuration #============================================================================= # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.3' # 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', 'sphinx.ext.intersphinx', # 'sphinx.ext.viewcode', # 3rd part extensions 'sphinxcontrib.fulltoc', # 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_links', # add "issue" role 'cloud_sptheme.ext.issue_tracker', # allow table column alignment styling 'cloud_sptheme.ext.table_styling', # monkeypatch sphinx to support a few extra things we can't do with extensions. 'cloud_sptheme.ext.autodoc_sections', 'cloud_sptheme.ext.autoattribute_search_bases', 'cloud_sptheme.ext.docfield_markup', 'cloud_sptheme.ext.escaped_samp_literals', ] # 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" updated = datetime.date.today().isoformat() copyright = "2008-%d, %s. Last Updated %s" % (datetime.date.today().year, author, updated) # 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) if ".dev" in release: tags.add("devcopy") if 'for-pypi' in options: tags.add("pypi") # 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."] # appended to all pages rst_epilog = "\n.. |updated| replace:: %s\n" % updated #============================================================================= # 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, sidebarbgcolor='transparent', small_sidebar_bg_color='#EBEBEB', bodytrimcolor='transparent', max_width="12.5in", sidebarwidth="3in", large_sidebar_width="3.5in", hyphenation_language="en", # headfont='"Bitstream Vera Sans", sans-serif', colored_object_prefixes="all", # bodyfont='arial, helvetica, sans-serif', relbarbgcolor='#C74A29', footerbgcolor='#733610', sectionbgcolor='#FB8A45', rubricbgcolor='#FFB657', sidebarlinkcolor='#6A3051', link_hover_text_color='#ff0000', link_hover_trim_color='#ddb1b8', toc_local_bg_color='#FFE8C4', toc_local_trim_color='#FFC68A', ) 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") # 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 = os.path.join("_static", "logo.ico") # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. ##html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = {'**': ['searchbox.html', 'globaltoc.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.7.1/docs/dev-requirements.txt0000644000175000017500000000006013015205366021270 0ustar biscuitbiscuit00000000000000hg+https://bitbucket.org/ecollins/cloud_sptheme passlib-1.7.1/docs/other.rst0000644000175000017500000000036113015205366017107 0ustar biscuitbiscuit00000000000000=================== Other Documentation =================== Additional pages and documentation which don't fit anywhere else: .. toctree:: :titlesonly: :maxdepth: 1 faq modular_crypt_format history/index copyright passlib-1.7.1/docs/lib/0000755000175000017500000000000013043774617016015 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/lib/passlib.hash.cta_pbkdf2_sha1.rst0000644000175000017500000000442712257351267024045 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.7.1/docs/lib/passlib.hash.mysql323.rst0000644000175000017500000000433313016611237022511 0ustar biscuitbiscuit00000000000000.. index:: MySQL; OLD_PASSWORD() ======================================================================== :class:`passlib.hash.mysql323` - MySQL 3.2.3 password hash ======================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. currentmodule:: passlib.hash 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 >>> # hash password >>> mysql323.hash("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.7.1/docs/lib/passlib.hash.phpass.rst0000644000175000017500000000553712257351267022433 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.7.1/docs/lib/passlib.hash.atlassian_pbkdf2_sha1.rst0000644000175000017500000000370212257351267025250 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.7.1/docs/lib/passlib.hash.oracle10.rst0000644000175000017500000001124213016611237022517 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.oracle10` - Oracle 10g password hash ================================================================== .. include:: ../_fragments/trivial_hash_warning.rst .. currentmodule:: passlib.hash 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 >>> # hash password using specified username >>> hash = oracle10.hash("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.7.1/docs/lib/passlib.hash.postgres_md5.rst0000644000175000017500000000526013016611237023527 0ustar biscuitbiscuit00000000000000.. index:: Postgres; md5 hash ================================================================== :class:`passlib.hash.postgres_md5` - PostgreSQL MD5 password hash ================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. 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 >>> # hash password using specified username >>> hash = postgres_md5.hash("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.7.1/docs/lib/index.rst0000644000175000017500000000231713015205366017646 0ustar biscuitbiscuit00000000000000============= API Reference ============= The reference section contains documentation of Passlib's public API. These chapters are focused on providing detailed reference of the individual functions and classes; they will generally be cross-linked to any related walkthrough documentation (which tries to provide a higher-level synthetic view). .. rst-class:: float-right without-title .. note:: **Primary modules:** The primary modules that will be of interest are: * :mod:`passlib.hash` * :mod:`passlib.context` * :mod:`passlib.totp` * :mod:`passlib.exc` .. rst-class:: float-right without-title clear-right .. caution:: **Internal modules:** The following modules are mainly used internally, may change structure between releases, and are documented mainly for completeness: * :mod:`passlib.crypto` * :mod:`passlib.registry` * :mod:`passlib.utils` **Alphabetical module list:** .. toctree:: :titlesonly: :maxdepth: 1 passlib.apache passlib.apps passlib.context passlib.crypto passlib.exc passlib.ext.django passlib.hash passlib.hosts passlib.ifc passlib.pwd passlib.registry passlib.totp passlib.utils passlib-1.7.1/docs/lib/passlib.hash.mssql2000.rst0000644000175000017500000000641513016611237022560 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.mssql2000` - MS SQL 2000 password hash ================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. versionadded:: 1.6 .. 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 >>> # hash password >>> h = m20.hash("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.7.1/docs/lib/passlib.apps.rst0000644000175000017500000002177313016611237021144 0ustar biscuitbiscuit00000000000000================================================================== :mod:`passlib.apps` - Helpers for various applications ================================================================== .. module:: passlib.apps :synopsis: hashing & 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 Hashing a password is simple (and salt generation is handled automatically):: >>> hash = custom_app_context.hash("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.hash("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 hashing or verifying a postgres hash:: >>> from passlib.apps import postgres_context >>> # hashing a password... >>> postgres_context.hash("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.hash("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:`/narr/quickstart` for additional details. passlib-1.7.1/docs/lib/passlib.hash.dlitz_pbkdf2_sha1.rst0000644000175000017500000000622013015205366024404 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.binary.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.7.1/docs/lib/passlib.utils.compat.rst0000644000175000017500000000337612214647123022624 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.7.1/docs/lib/passlib.hash.ldap_other.rst0000644000175000017500000000332612257351267023250 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.7.1/docs/lib/passlib.exc.rst0000644000175000017500000000203413015205366020746 0ustar biscuitbiscuit00000000000000============================================ :mod:`passlib.exc` - Exceptions and warnings ============================================ .. 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:: PasswordTruncateError .. autoexception:: PasslibSecurityError .. autoexception:: UnknownHashError TOTP Exceptions --------------- .. autoexception:: TokenError .. autoexception:: MalformedTokenError .. autoexception:: InvalidTokenError .. autoexception:: UsedTokenError Warnings ======== .. autoexception:: PasslibWarning Minor Warnings -------------- .. autoexception:: PasslibConfigWarning .. autoexception:: PasslibHashWarning Critical Warnings ----------------- .. autoexception:: PasslibRuntimeWarning .. autoexception:: PasslibSecurityWarning passlib-1.7.1/docs/lib/passlib.hash.argon2.rst0000644000175000017500000001251613016611237022306 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.argon2` - Argon2 ================================================================== .. versionadded:: 1.7 .. currentmodule:: passlib.hash This hash provides support for the Argon2 [#argon2-home]_ password hash. Argon2(i) is a state of the art memory-hard password hash, and the winner of the 2013 Password Hashing Competition [#phc]_. It has seen active development and analysis in subsequent years, and while young, and is intended to replace :class:`~passlib.hash.pbkdf2_sha256`, :class:`~passlib.hash.bcrypt`, and :class:`~passlib.hash.scrypt`. It is one of the four hashes Passlib :ref:`recommends ` for new applications. This class can be used directly as follows:: >>> from passlib.hash import argon2 >>> # generate new salt, hash password >>> h = argon2.hash("password") >>> h '$argon2i$v=19$m=512,t=2,p=2$aI2R0hpDyLm3ltLa+1/rvQ$LqPKjd6n8yniKtAithoR7A' >>> # the same, but with an explicit number of rounds >>> argon2.using(rounds=4).hash("password") '$argon2i$v=19$m=512,t=4,p=2$eM+ZMyYkpDRGaI3xXmuNcQ$c5DeJg3eb5dskVt1mDdxfw' >>> # verify password >>> argon2.verify("password", h) True >>> argon2.verify("wrong", h) False .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: argon2() Argon2 Backends --------------- This class will use the first available of two possible backends: 1. `argon2_cffi `_, if installed. (this is the recommended option). 2. `argon2pure `_, if installed. If no backends are available, :meth:`hash` and :meth:`verify` will throw :exc:`~passlib.exc.MissingBackendError` when they are invoked. You can check which backend is in use by calling :meth:`!argon2.get_backend()`. Format & Algorithm ================== The Argon2 hash format is defined by the argon2 reference implementation. It's compatible with the :ref:`PHC Format ` and :ref:`modular-crypt-format`, and uses ``$argon2i$`` and ``$argon2d$`` as it's identifying prefixes for all its strings. An example hash (of ``password``) is: ``$argon2i$v=19$m=512,t=3,p=2$c29tZXNhbHQ$SqlVijFGiPG+935vDSGEsA`` This string has the format :samp:`$argon2{X}$v={V}$m={M},t={T},p={P}${salt}${digest}`, where: * :samp:`{X}` is either ``i`` or ``d``, depending on the argon2 variant (``i`` in the example). * :samp:`{V}` is an integer representing the argon2 revision. the value (when rendered into hexidecimal) matches the argon2 version (in the example, ``v=19`` corresponds to 0x13, or Argon2 v1.3). * :samp:`{M}` is an integer representing the variable memory cost, in kibibytes (512kib in the example). * :samp:`{T}` is an integer representing the variable time cost, in linear iterations. (3 in the example). * :samp:`{P}` is a parallelization parameter, which controls how much of the hash calculation is parallelization (2 in the example). * :samp:`{salt}` - this is the base64-encoded version of the raw salt bytes passed into the Argon2 function (``c29tZXNhbHQ`` in the example). * :samp:`{digest}` - this is the base64-encoded version of the raw derived key bytes returned from the Argon2 function. Argon2 supports a variable checksum size, though the hashes in passlib will typically be 16 bytes, resulting in a 22 byte digest (``SqlVijFGiPG+935vDSGEsA`` in the example). All integer values are encoded uses ascii decimal, with no leading zeros. All byte strings are encoded using the standard base64 encoding, but without any trailing padding ("=") chars. .. note:: The :samp:`v={version}$` segment was added in Argon2 v1.3; older version Argon2 v1.0 hashes may not include this portion. The Argon2 specification also supports an optional :samp:`,data={data}` suffix following :samp:`p={parallelism}`; but this is not consistently or fully supported. 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 handed off to the Argon2 function. A specified number of bytes (16 byte default in passlib) returned result are encoded as the checksum. See ``_ for the canonical description of the Argon2 hash. Security Issues =============== Argon2 is relatively new compared to other password hash algorithms, having started life in 2013, and thus may still harbor some undiscovered issues. That said, it's one of *very* few which were designed explicitly with password hashing in mind; and draws strongly on the lessons of the algorithms before it. As of the release of Passlib 1.7, it has no known major security issues. Deviations ========== * While passlib supports verifying type "d" Argon2 hashes, it does not support generating them. This is a deliberate choice, since type "d" is explicitly not designed for password hashing. * This implementation currently encodes all unicode passwords using UTF-8 before hashing, other implementations may vary, or offer a configurable encoding; though UTF-8 is assumed to be the default. .. rubric:: Footnotes .. [#argon2-home] the Argon2 homepage - ``_ .. [#phc] 2012 Password Hashing Competition - ``_ passlib-1.7.1/docs/lib/passlib.hash.sha256_crypt.rst0000644000175000017500000001342613016611237023350 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 four hashes Passlib :ref:`recommends ` for new applications. This class can be used directly as follows:: >>> from passlib.hash import sha256_crypt >>> # generate new salt, hash password >>> hash = sha256_crypt.hash("password") >>> hash '$5$rounds=80000$wnsT7Yr92oJoP28r$cKhJImk5mfuSKV9b3mumNzlbstFUplKtQXXMo4G6Ep5' >>> # same, but with explict number of rounds >>> sha256_crypt.using(rounds=12345).hash("password") '$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]_. Security Issues =============== * The algorithm's initialization stage contains a loop which varies linearly with the square of the password size; and further loops which vary linearly with the password size * rounds. - This means an attacker could provide a maliciously large password at the login screen to attempt a DOS on a publically visible login. For example, a 32kib password would require hashing 1gib of data. Passlib mitigates this by limiting the maximum password size to 4k by default. - An attacker could also theoretically determine a password's size by observing the time taken on a successful login, and then attempting verification themselves to find the size password which has an equivalent delay. This has not been applied in practice, probably due to the fact that (for normal passwords < 64 bytes), the contribution of the password size to the overall time taken is below the observable noise level when evesdropping on the timings of successful logins for a single user. 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.7.1/docs/lib/passlib.apache.rst0000664000175000017500000000564413043772621021430 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. .. versionchanged:: 1.7 These classes will now preserve blank lines and "#" comments when updating htpasswd files; previous releases would throw a parse error. .. 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.7.1/docs/lib/passlib.hash.bigcrypt.rst0000644000175000017500000001332713015205366022743 0ustar biscuitbiscuit00000000000000======================================================================= :class:`passlib.hash.bigcrypt` - BigCrypt ======================================================================= .. include:: ../_fragments/trivial_hash_warning.rst .. 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.7.1/docs/lib/passlib.hash.apr_md5_crypt.rst0000644000175000017500000000317613015205366023671 0ustar biscuitbiscuit00000000000000.. index:: Apache; md5 password hash ====================================================================== :class:`passlib.hash.apr_md5_crypt` - Apache's MD5-Crypt variant ====================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. 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.7.1/docs/lib/passlib.hash.msdcc2.rst0000644000175000017500000001004113016611237022260 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 >>> # hash password using specified username >>> hash = msdcc2.hash("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.7.1/docs/lib/passlib.crypto.digest.rst0000644000175000017500000000237013015205366022770 0ustar biscuitbiscuit00000000000000============================================================= :mod:`passlib.crypto.digest` - Hash & Related Helpers ============================================================= .. module:: passlib.crypto.digest :synopsis: Internal cryptographic helpers .. versionadded:: 1.7 This module provides various cryptographic support functions used by Passlib to implement the various password hashes it provides, as well as paper over some VM & version incompatibilities. Hash Functions ============== .. autofunction:: norm_hash_name .. autofunction:: lookup_hash .. rst-class:: float-center .. note:: :func:`!lookup_hash` supports all hashes available directly in :mod:`hashlib`, as well as offered through :func:`hashlib.new`. It will also fallback to passlib's builtin MD4 implementation if one is not natively available. .. autoclass:: HashInfo() .. HMAC Functions ============== .. autofunction:: compile_hmac PKCS#5 Key Derivation Functions =============================== .. autofunction:: pbkdf1 .. autofunction:: pbkdf2_hmac .. data:: PBKDF2_BACKENDS List of the pbkdf2 backends in use (listed in order of priority). .. versionadded:: 1.7 .. note:: The details of PBKDF1 and PBKDF2 are specified in :rfc:`2898`. passlib-1.7.1/docs/lib/passlib.utils.binary.rst0000644000175000017500000000544113015214076022615 0ustar biscuitbiscuit00000000000000===================================================== :mod:`passlib.utils.binary` - Binary Helper Functions ===================================================== .. module:: passlib.utils.binary :synopsis: internal helpers for binary data .. 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. Constants ========= .. 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. .. TODO: document the other constants 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 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 .. autofunction:: b64s_encode .. autofunction:: b64s_decode .. autofunction:: b32encode .. autofunction:: b32decode .. .. data:: AB64_CHARS Variant of standard Base64 character map used by some custom Passlib hashes (see :func:`ab64_encode`). passlib-1.7.1/docs/lib/passlib.hash.sha1_crypt.rst0000644000175000017500000001055313015205366023173 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.7.1/docs/lib/passlib.utils.rst0000644000175000017500000001105513015205366021332 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 =============== .. function:: 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 .. versionchanged:: 1.7 This is an alias for stdlib's :func:`hmac.compare_digest` under Python 3.3 and up. .. 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 .. 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.binary passlib.utils.des passlib.utils.pbkdf2 .. passlib.utils.decor passlib.utils.compat passlib-1.7.1/docs/lib/passlib.hash.nthash.rst0000644000175000017500000000506113016611237022400 0ustar biscuitbiscuit00000000000000.. index:: Windows; NT hash ================================================================== :class:`passlib.hash.nthash` - Windows' NT-HASH ================================================================== .. include:: ../_fragments/trivial_hash_warning.rst .. versionadded:: 1.6 .. 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 >>> # hash password >>> h = nthash.hash("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 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.hash` 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.7.1/docs/lib/passlib.hash.sun_md5_crypt.rst0000644000175000017500000002106312257351267023720 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.7.1/docs/lib/passlib.hash.md5_crypt.rst0000644000175000017500000001654413016611237023031 0ustar biscuitbiscuit00000000000000.. index:: Cisco; Type 5 hash ================================================================== :class:`passlib.hash.md5_crypt` - MD5 Crypt ================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. 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, hash password >>> h = md5_crypt.hash("password") >>> h '$1$3azHgidD$SrJPt7B.9rekpmwJwtON31' >>> # verify the password >>> md5_crypt.verify("password", h) True >>> md5_crypt.verify("secret", h) False >>> # hash password using cisco-compatible 4-char salt >>> md5_crypt.using(salt_size=4).hash("password") '$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.7.1/docs/lib/passlib.crypto.rst0000644000175000017500000000111013015205366021501 0ustar biscuitbiscuit00000000000000====================================================== :mod:`passlib.crypto` - Cryptographic Helper Functions ====================================================== .. module:: passlib.crypto :synopsis: internal cryptographic helpers for implementing password hashes This module is primarily used as an internal support module. It contains cryptography utility functions used by Passlib. However, end-user applications may find some of the functions useful. It contains the following submodules: .. toctree:: :maxdepth: 1 passlib.crypto.digest passlib.crypto.des passlib-1.7.1/docs/lib/passlib.context-tutorial.rst0000644000175000017500000000015413015205366023515 0ustar biscuitbiscuit00000000000000:orphan: .. redirect stub .. seealso:: This page has been moved to :doc:`/narr/context-tutorial` passlib-1.7.1/docs/lib/passlib.hash.mssql2005.rst0000644000175000017500000000535613016611237022570 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.mssql2005` - MS SQL 2005 password hash ================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. versionadded:: 1.6 .. 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 >>> # hash password >>> h = m25.hash("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.7.1/docs/lib/passlib.hash.bsdi_crypt.rst0000664000175000017500000001417713043772621023274 0ustar biscuitbiscuit00000000000000================================================================================= :class:`passlib.hash.bsdi_crypt` - BSDi Crypt ================================================================================= .. include:: ../_fragments/insecure_hash_warning.rst .. currentmodule:: passlib.hash 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, hash password >>> hash = bsdi_crypt.hash("password") >>> hash '_7C/.Bf/4gZk10RYRs4Y' >>> # same, but with explict number of rounds >>> bsdi_crypt.using(rounds=10001).hash("password") '_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 20 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.7.1/docs/lib/passlib.crypto.des.rst0000644000175000017500000000172313015205366022265 0ustar biscuitbiscuit00000000000000============================================== :mod:`passlib.crypto.des` - DES routines ============================================== .. module:: passlib.crypto.des :synopsis: routines for performing DES encryption .. versionchanged:: 1.7 This module was relocated from :mod:`!passlib.utils.des`; the old location will be removed in Passlib 2.0. .. 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.7.1/docs/lib/passlib.hash.msdcc.rst0000644000175000017500000000625613016611237022213 0ustar biscuitbiscuit00000000000000.. index:: single: Windows; Domain Cached Credentials see: mscash; msdcc see: mscache; msdcc ====================================================================== :class:`passlib.hash.msdcc` - Windows' Domain Cached Credentials ====================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. versionadded:: 1.6 .. 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 >>> # hash password using specified username >>> hash = msdcc.hash("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.7.1/docs/lib/passlib.hash.django_std.rst0000644000175000017500000002115513016611237023231 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.10. .. .. 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.10 Hashes ================== Argon2 ------ Django 1.10 added a wrapper for Argon2: .. class:: django_argon2 This class implements Django 1.10's Argon2 wrapper, and follows the :ref:`password-hash-api`. This is identical to :class:`!argon2` itself, but with the Django-specific prefix ``"argon2"`` prepended. See :doc:`argon2 ` for more details, the usage and behavior is identical. This should be compatible with the hashes generated by Django 1.10's :class:`!Argon2PasswordHasher` class. .. versionadded:: 1.7 This hash has the exact same structure as :mod:`passlib.hash.argon2`, except that it has the prefix ``argon2`` added. For example, the django_argon2 hash...:: argon2$argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A ...corresponds to the argon2 hash:: argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A Django 1.6 Hashes ================= Django 1.6 added one new hash: Bcrypt SHA256 ------------- .. autoclass:: django_bcrypt_sha256 This hash has the exact same structure as :class:`~passlib.hash.bcrypt`, except that it has the prefix ``bcrypt$`` added. For example, the django_bcrypt_sha256 hash...:: bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu ...has the same structure as the bcrypt hash.:: $2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu That said, the hash is calculated slightly differently... the password is run through sha256(), the result encoded to lowercase hexadecimal, and then handed to bcrypt proper. This very similar to passlib's :class:`~passlib.hash.bcrypt_sha256`, and addresses the same set of issues. .. _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 >>> # hash password >>> h = handler.hash("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 >>> # hash password >>> h = handler.hash("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.7.1/docs/lib/passlib.hash.cisco_pix.rst0000644000175000017500000001675013043701662023104 0ustar biscuitbiscuit00000000000000.. index:: Cisco; PIX hash ================================================================== :class:`passlib.hash.cisco_pix` - Cisco PIX MD5 hash ================================================================== .. currentmodule:: passlib.hash .. include:: ../_fragments/insecure_hash_warning.rst .. versionadded:: 1.6 Overview ======== .. include:: ../_fragments/asa_verify_callout.rst The :class:`cisco_asa` class implements the "encrypted" password hash algorithm commonly found on Cisco ASA systems. The companion :class:`cisco_pix` class implements the older variant found on Cisco PIX. Aside from internal differences, and slightly different limitations, the two hashes have the same format, and in some cases the same output. These classes can be used directly to generate or verify a hash for a specific user. Specifying the user account name is required for this hash:: >>> from passlib.hash import cisco_asa >>> # hash password using specified username >>> hash = cisco_asa.hash("password", user="user") >>> hash 'A5XOy94YKDPXCo7U' >>> # verify correct password >>> cisco_asa.verify("password", hash, user="user") True >>> # verify correct password w/ wrong username >>> cisco_asa.verify("password", hash, user="other") False >>> # verify incorrect password >>> cisco_asa.verify("letmein", hash, user="user") False The main "enable" password can be hashes / verified just by omitting the ``user`` parameter, or setting ``user=""``:: >>> # hash password without associated user account >>> hash2 = cisco_asa.hash("password") >>> hash2 'NuLKvvWGg.x9HEKO' >>> # verify password without associated user account >>> cisco_asa.verify("password", hash2) True .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: cisco_pix() .. autoclass:: cisco_asa() .. note:: These hash algorithms have a context-sensitive peculiarity. They take in an optional username to salt the hash, but have 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 & ASA hashes consist of a 12 byte digest, encoded as a 16 character :data:`HASH64 `-encoded string. An example hash (of ``"password"``, with user ``""``) is ``"NuLKvvWGg.x9HEKO"``. The PIX / ASA digests are calculated as follows: 1. The password is encoded using ``UTF-8`` (though entering non-ASCII characters is subject to interface-specific issues, and may lead to problems such as double-encoding). If the result is greater than 16 bytes (for PIX), or 32 bytes (for ASA), the password is not allowed -- it will be rejected when set, and simplify not verify during authentication. 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. If the user account is 1-3 bytes, it is repeated until all 4 bytes are filled up (e.g. "usr" becomes "usru"). For :class:`!cisco_asa`, this step is omitted if the password is 28 bytes or more. 3. The password+user string is truncated, or right-padded with NULLs, until it's 16 bytes in size. For :class:`!cisco_asa`, if the password+user string is 16 or more bytes, a padding size of 32 is used instead. 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 `. .. versionchanged:: 1.7.1 Updated to reflect current understanding of the algorithm. 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 ``password+user`` 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 tries to adhere to the canonical Cisco implementation, but without an official specification, there may be other unknown deviations. The following are known issues: * Unicode Policy: ASA documentation [#charset]_ indicates it uses UTF-8 encoding, and Passlib does as well. However, some ASA interfaces have issues such as: ASDM may double-encode unicode characters, and SSH connections may drop non-ASCII characters entirely. * How usernames are added is not entirely pinned down. Under ASA, 3-character usernames have their last character repeated to make a string of length 4. It is currently assumed that a similar repetition would be applied to usernames of 1-2 characters, and that this applies to PIX as well; though neither assumption has been confirmed. * .. _passlib-asa96-bug: **Passlib 1.7.1 Bugfix**: Prior releases of Passlib had a number of issues with their implementation of the PIX & ASA algorithms. As of 1.7.1, the reference vectors were greatly expanded, and then tested against an ASA 9.6 system. This revealed a number of errors in passlib's implementation, which under the following conditions would create hashes that were unverifiable on a Cisco system: - PIX and ASA: Usernames containing 1-3 characters were not appended correctly (step 2, above). - ASA omits the user entirely (step 2, above) for passwords with >= 28 characters, not >= 27. Non-enable passwords of exactly 27 characters were previous hashed incorrectly. - ASA's padding size decision (step 3, above) is made after the user has been appended, not before. This caused prior releases to incorrectly hash non-enable passwords of length 13-15. Anyone relying on cisco_asa or cisco_pix should upgrade to Passlib 1.7.1 or newer to avoid these issues. .. rubric:: Footnotes .. [#] Description of PIX algorithm - ``_ .. [#] Message threads hinting at how username is handled - ``_, ``_ .. [#] Partial description of ASA algorithm - ``_ .. [#charset] Character set used by ASA 8.4 - ``_ passlib-1.7.1/docs/lib/passlib.utils.pbkdf2.rst0000644000175000017500000000237713015205366022510 0ustar biscuitbiscuit00000000000000========================================================================== :mod:`passlib.utils.pbkdf2` - PBKDF2 key derivation algorithm [deprecated] ========================================================================== .. module:: passlib.utils.pbkdf2 :synopsis: PBKDF2 and related key derivation algorithms .. warning:: This module has been deprecated as of Passlib 1.7, and will be removed in Passlib 2.0. The functions in this module have been replaced by equivalent (but not identical) functions in the :mod:`passlib.crypto` module. 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 passlib-1.7.1/docs/lib/passlib.hash.mysql41.rst0000644000175000017500000000335313015205366022430 0ustar biscuitbiscuit00000000000000.. index:: MySQL; PASSWORD() ===================================================================== :class:`passlib.hash.mysql41` - MySQL 4.1 password hash ===================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. currentmodule:: passlib.hash 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.7.1/docs/lib/passlib.hash.rst0000644000175000017500000002145313043701620021113 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 inherit from the :mod:`~passlib.ifc.PasswordHash` base interface. The following pages describe each hash in detail, including its format, underlying algorithm, and known security issues. .. rst-class:: float-center .. danger:: **Many of the hash algorithms listed below are *NOT* secure.** Passlib supports a wide array of hash algorithms, primarily to support legacy data and systems. If you want to choose a secure algorithm for a new application, see the :doc:`Quickstart Guide `. .. rst-class:: float-center .. seealso:: :ref:`hash-tutorial` -- for general usage examples .. _mcf-hashes: Unix Hashes =========== Aside from "archaic" schemes such as :class:`!des_crypt`, most of the password hashes supported by modern Unix flavors adhere to the :ref:`modular crypt format `, allowing them to be easily distinguished when used within the same file. The basic of format :samp:`${scheme}${hash}` has also been adopted for use by other applications and password hash schemes. .. _standard-unix-hashes: .. rst-class:: toc-always-open Active Unix Hashes ------------------ All the following schemes are actively in use by various Unix flavors to store user passwords They all follow the modular crypt format. .. toctree:: :maxdepth: 1 passlib.hash.bcrypt passlib.hash.sha256_crypt passlib.hash.sha512_crypt Special note should be made of the following fallback helper, which is not an actual hash scheme, but implements the "disabled account marker" found in many Linux & BSD password files: .. toctree:: :maxdepth: 1 passlib.hash.unix_disabled .. rst-class:: toc-always-open Deprecated Unix Hashes ---------------------- The following schemes are supported by various Unix systems using the modular crypt format, but are no longer considered secure, and have been deprecated in favor of the `Active Unix Hashes`_ (above). * :class:`passlib.hash.bsd_nthash` - FreeBSD's MCF-compatible encoding of :doc:`nthash ` digests .. toctree:: :maxdepth: 1 passlib.hash.md5_crypt passlib.hash.sha1_crypt passlib.hash.sun_md5_crypt .. _archaic-unix-schemes: .. rst-class:: toc-always-open Archaic Unix Hashes ------------------- The following schemes are supported by certain Unix systems, but are considered particularly archaic: Not only do they predate the modular crypt format, but they're based on the outmoded DES block cipher, and are woefully insecure: .. toctree:: :maxdepth: 1 passlib.hash.des_crypt passlib.hash.bsdi_crypt passlib.hash.bigcrypt passlib.hash.crypt16 Other "Modular Crypt" Hashes ============================ The :ref:`modular crypt format ` is a loose standard for password hash strings which started life under the Unix operating system, and is used by many of the Unix hashes (above). However, it's it's basic :samp:`${scheme}${hash}` format has also been adopted by a number of application-specific hash algorithms: .. rst-class:: toc-always-open Active Hashes ------------- While most of these schemes generally require an application-specific implementation, natively 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.argon2 passlib.hash.bcrypt_sha256 passlib.hash.phpass passlib.hash.pbkdf2_digest passlib.hash.scram passlib.hash.scrypt .. rst-class:: toc-always-open Deprecated Hashes ----------------- The following are some additional application-specific hashes which are still occasionally seen, use the modular crypt format, but are rarely used or weak enough that they have been deprecated: .. toctree:: :maxdepth: 1 passlib.hash.apr_md5_crypt passlib.hash.cta_pbkdf2_sha1 passlib.hash.dlitz_pbkdf2_sha1 .. _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. .. _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 .. rst-class:: toc-always-toggle Cisco Hashes ============ .. TODO: What was/were IOS types 1, 2, 3, and 6? Don't see many references. Think type 6 is a reversible encryption format ala type 7, per https://supportforums.cisco.com/discussion/11733226/when-use-type-6-encrypted-or-type-7-encrypted **Cisco IOS** The following hashes are used in various places on Cisco IOS, and are usually referred to by a Cisco-assigned "type" code: .. rst-class:: hidden .. toctree:: :maxdepth: 1 passlib.hash.cisco_type7 * :doc:`passlib.hash.md5_crypt ` -- "Type 5" hashes are actually just the standard Unix MD5-Crypt hash, the format is identical. * :doc:`passlib.hash.cisco_type7 ` -- "Type 7" isn't actually a hash, but a reversible encoding designed to obscure passwords from idle view. * "Type 8" hashes are based on PBKDF2-HMAC-SHA256; but not currently supported by passlib (:issue:`87`). * "Type 9" hashes are based on scrypt; but not currently supported by passlib (:issue:`87`). **Cisco PIX & ASA** Separately from this, Cisco PIX & ASA firewalls have their own hash formats, generally identified by the "format" parameter in the :samp:`username {user} password {hash} {format}` config line they occur in. The following are known & handled by passlib: .. rst-class:: hidden .. toctree:: :maxdepth: 1 passlib.hash.cisco_pix passlib.hash.cisco_asa * :doc:`passlib.hash.cisco_pix ` -- PIX "encrypted" hashes use a simple unsalted MD5-based algorithm. * :doc:`passlib.hash.cisco_asa ` -- ASA "encrypted" hashes use a similar algorithm to PIX, with some minor improvements. * ASA "nt-encrypted" hashes are the same as :class:`passlib.hash.nthash`, except that they use base64 encoding rather than hexadecimal. * ASA 9.5 added support for "pbkdf2" hashes (based on PBKDF2-HMAC-SHA512); which aren't currently supported by passlib (:issue:`87`). .. _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.django_std passlib.hash.grub_pbkdf2_sha512 passlib.hash.hex_digests passlib.hash.plaintext .. rubric:: Footnotes .. [#openldap] OpenLDAP homepage - ``_. passlib-1.7.1/docs/lib/passlib.hash.des_crypt.rst0000644000175000017500000001251313016611237023107 0ustar biscuitbiscuit00000000000000======================================================================= :class:`passlib.hash.des_crypt` - DES Crypt ======================================================================= .. include:: ../_fragments/trivial_hash_warning.rst .. 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, hash password >>> hash = des_crypt.hash("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.7.1/docs/lib/passlib.hash.sha512_crypt.rst0000644000175000017500000000342012257351267023346 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.7.1/docs/lib/passlib.hash.scrypt.rst0000644000175000017500000001210013041176777022444 0ustar biscuitbiscuit00000000000000================================================================== :class:`passlib.hash.scrypt` - SCrypt ================================================================== .. versionadded:: 1.7 .. currentmodule:: passlib.hash This is a custom hash scheme provided by Passlib which allows storing password hashes generated using the SCrypt [#scrypt-home]_ key derivation function, and is designed as the of a new generation of "memory hard" functions. .. warning:: Be careful when using this algorithm, as the memory and CPU requirements needed to achieve adequate security are generally higher than acceptable for heavily used production systems [#scrypt-cost]_. This is because (unlike many password hashes), increasing the rounds value of scrypt will increase the *memory* required as well as the time. Unless you know what you're doing, **You probably want** :doc:`argon2 ` **instead.** This class can be used directly as follows:: >>> from passlib.hash import scrypt >>> # generate new salt, hash password >>> h = scrypt.hash("password") >>> h '$scrypt$ln=16,r=8,p=1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E' >>> # the same, but with an explicit number of rounds >>> scrypt.using(rounds=8).hash("password") '$scrypt$ln=8,r=8,p=1$WKs1xljLudd6z9kbY0wpJQ$yCR4iDZYDKv+iEJj6yHY0lv/epnfB6f/w1EbXrsJOuQ' >>> # verify password >>> scrypt.verify("password", h) True >>> scrypt.verify("wrong", h) False .. note:: It is strongly recommended that you install `scrypt `_ when using this hash. .. seealso:: the generic :ref:`PasswordHash usage examples ` Interface ========= .. autoclass:: scrypt() Scrypt Backends --------------- This class will use the first available of two possible backends: 1. The C-accelerated `scrypt `_ package, if installed. 2. A pure-python implementation of SCrypt, built into Passlib. .. warning:: *It is strongly recommended to install the external scrypt package*. The pure-python backend is intended as a reference and last-resort implementation only; it is 10-100x too slow to be usable in production at a secure ``rounds`` cost. Format & Algorithm ================== This Scrypt hash format is compatible with the :ref:`PHC Format ` and :ref:`modular-crypt-format`, and uses ``$scrypt$`` as the identifying prefix for all its strings. An example hash (of ``password``) is: ``$scrypt$ln=16,r=8,p=1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E`` This string has the format :samp:`$scrypt$ln={logN},r={R},p={P}${salt}${checksum}`, where: * :samp:`{logN}` is the exponent for calculating SCRYPT's cost parameter (N), encoded as a decimal digit, (logN is 16 in the example, corresponding to n = 2**16 = 65536). * :samp:`{R}` is the value of SCRYPT's block size parameter (r), encoded as a decimal digit, (r is 8 in the example). * :samp:`{P}` is the value of SCRYPT's parallel count parameter (p), encoded as a decimal digit, (p is 1 in the example). * :samp:`{salt}` - this base64 encoded salt bytes passed into the SCRYPT function (``aM15713r3Xsvxbi31lqr1Q`` in the example). * :samp:`{checksum}` - this is the base64 encoded derived key bytes returned from the SCRYPT function. This hash currently always uses 32 bytes, resulting in a 43-character checksum. (``nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E`` in the example). All byte strings are encoded using the standard base64 encoding, but without any trailing padding ("=") chars. The password is encoded into UTF-8 if not already encoded, and run throught the SCRYPT function; along with the salt, and the values of n, r, and p. The first 32 bytes of the returned result are encoded as the checksum. See ``_ for the canonical description of the scrypt kdf. Security Issues =============== `SCrypt `__ is the first in a class of "memory-hard" key derivation functions. Initially, it looked very promising as a replacement for BCrypt, PBKDF2, and SHA512-Crypt. However, the fact that it's ``N`` parameter controls both time *and* memory cost means the two cannot be varied completely independantly. This eventually proved to be problematic, as ``N`` values required for even BCrypt levels of security resulting in memory requirements that were unacceptable on most production systems. .. seealso:: :class:`~passlib.hash.argon2`, a next generation memory-hard KDF designed as the successor to SCrypt. .. rubric:: Footnotes .. [#scrypt-home] the SCrypt KDF homepage - ``_ .. [#scrypt-cost] posts discussing security implications of scrypt's tying memory cost to calculation time - ``_, ``_, ``_ passlib-1.7.1/docs/lib/passlib.hash.scram.rst0000644000175000017500000001524613016611237022226 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.crypto.digest.pbkdf2_hmac` 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, hash password against default list of algorithms >>> hash = scram.hash("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.using(rounds=8000).hash("password") '$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.using(rounds=1000, algs="sha-1,sha-256,md5").hash("password") >>> 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.binary.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.binary.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.7.1/docs/lib/passlib.pwd.rst0000644000175000017500000000464213015205366020770 0ustar biscuitbiscuit00000000000000.. module:: passlib.pwd :synopsis: password generation helpers ================================================= :mod:`passlib.pwd` -- Password generation helpers ================================================= .. versionadded:: 1.7 Password Generation =================== .. rst-class:: float-center .. warning:: Before using these routines, make sure your system's RNG entropy pool is secure and full. Also make sure that :func:`!genword` or :func:`!genphrase` is called with a sufficiently high ``entropy`` parameter the intended purpose of the password. .. autofunction:: genword(entropy=None, length=None, charset="ascii_62", chars=None, returns=None) .. autofunction:: genphrase(entropy=None, length=None, wordset="eff_long", words=None, sep=" ", returns=None) Predefined Symbol Sets ====================== The following predefined sets are used by the generation functions above, but are exported by this module for general use: .. object:: default_charsets Dictionary mapping charset name -> string of characters, used by :func:`genword`. See that function for a list of predefined charsets present in this dict. .. object:: default_wordsets Dictionary mapping wordset name -> tuple of words, used by :func:`genphrase`. See that function for a list of predefined wordsets present in this dict. (Note that this is actually a special object which will lazy-load wordsets from disk on-demand) Password Strength Estimation ============================ Passlib does not current offer any password strength estimation routines. However, the (javascript-based) `zxcvbn `_ project is a very good choice. There are a few python ports of ZCVBN library, though as of 2016-11, none of them seem active and up to date. The following is a list of known ZCVBN python ports, though it's not clear which of these is active and/or official: * https://github.com/dropbox/python-zxcvbn -- seemingly official python version, but not updated since 2013, and not published on pypi. * https://github.com/rpearl/python-zxcvbn -- fork of official version, also not updated since 2013, but released to pypi as `"zxcvbn" `_. * https://github.com/gordon86/python-zxcvbn -- fork that has some updates as of july 2015, released to pypi as `"zxcvbn-py3" `_ (and compatible with 2 & 3, despite the name). passlib-1.7.1/docs/lib/passlib.hash.ldap_pbkdf2_digest.rst0000644000175000017500000000262012257351267024632 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.7.1/docs/lib/passlib.hash.unix_disabled.rst0000644000175000017500000000276313016611237023733 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 >>> # 'hashing' a password always results in "!" or "*" >>> unix_disabled.hash("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.7.1/docs/lib/passlib.hosts.rst0000644000175000017500000001146713016611237021340 0ustar biscuitbiscuit00000000000000============================================ :mod:`passlib.hosts` - OS Password Handling ============================================ .. module:: passlib.hosts :synopsis: hashing & verifying operating system passwords This module provides some preconfigured :ref:`CryptContext ` instances for hashing & 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 Hashing a password is simple (and salt generation is handled automatically):: >>> hash = linux_context.hash("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.hash("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 hashing 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.7.1/docs/lib/passlib.hash.bcrypt.rst0000644000175000017500000002321213041175254022416 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 four hashes Passlib :ref:`recommends ` for new applications. This class can be used directly as follows:: >>> from passlib.hash import bcrypt >>> # generate new salt, hash password >>> h = bcrypt.hash("password") >>> h '$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy' >>> # the same, but with an explicit number of rounds >>> bcrypt.using(rounds=8).hash("password") '$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:`hash` 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 a number of identifying prefixes: ``$2$``, ``$2a$``, ``$2x$``, ``$2y$``, and ``$2b$``. Each prefix indicates a different revision of the BCrypt algorithm; and all but the ``$2b$`` identifier are considered deprecated. An example hash (of ``password``) is: ``$2b$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-1.2, 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. (crypt_blowfish 1.3 switched to the ``$2b$`` standard as the default) .. 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.7.1/docs/lib/passlib.hash.oracle11.rst0000644000175000017500000000546113016611237022526 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, hash password >>> hash = oracle11.hash("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.7.1/docs/lib/passlib.utils.handlers.rst0000644000175000017500000002067213015205366023136 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 # [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.7.1/docs/lib/passlib.hash.pbkdf2_digest.rst0000644000175000017500000001044213016611237023621 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 four 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, hash password >>> hash = pbkdf2_sha256.hash("password") >>> hash '$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M' >>> # same, but with an explicit number of rounds and salt length >>> pbkdf2_sha256.using(rounds=8000, salt_size=10).hash("password") '$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.crypto.digest.pbkdf2_hmac` 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.binary.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.7.1/docs/lib/passlib.hash.cisco_asa.rst0000644000175000017500000000125113043701620023030 0ustar biscuitbiscuit00000000000000.. index:: Cisco; ASA hash ================================================================== :class:`passlib.hash.cisco_asa` - Cisco ASA MD5 hash ================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. currentmodule:: passlib.hash .. versionadded:: 1.7 .. include:: ../_fragments/asa_verify_callout.rst The :class:`!cisco_asa` class provides support for Cisco ASA "encrypted" hash format. This is a revision of the older :class:`!cisco_pix` hash; and the usage and format is the same. **See the** :doc:`cisco_pix ` **documentation page** for combined details of both these classes. passlib-1.7.1/docs/lib/passlib.hash.ldap_crypt.rst0000644000175000017500000000460613016611237023260 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 >>> # hash password >>> hash = ldap_md5_crypt.hash("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.7.1/docs/lib/passlib.hash.hex_digests.rst0000644000175000017500000000405513016611237023423 0ustar biscuitbiscuit00000000000000=============================================================== :samp:`passlib.hash.hex_{digest}` - Generic Hexadecimal Digests =============================================================== .. danger:: 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 >>> # hash password >>> h = hex_sha1.hash("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.7.1/docs/lib/passlib.totp.rst0000644000175000017500000001401113015205366021153 0ustar biscuitbiscuit00000000000000.. module:: passlib.totp :synopsis: totp / two factor authentaction ======================================================= :mod:`passlib.totp` -- TOTP / Two Factor Authentication ======================================================= .. versionadded:: 1.7 Overview ======== The :mod:`!passlib.totp` module provides a number of classes for implementing two-factor authentication (2FA) using the TOTP [#totpspec]_ specification. This page provides a reference to all the classes and methods in this module. Passlib's TOTP support is centered around the :class:`TOTP` class. There are also some additional helpers, including the :class:`AppWallet` class, which helps to securely encrypt TOTP keys for storage. .. seealso:: * :ref:`TOTP Tutorial ` -- Overview of this module and walkthrough of how to use it. TOTP Class ========== .. autoclass:: TOTP(key=None, format="base32", \*, new=False, \*\*kwds) See below for all the :class:`!TOTP` methods & attributes... Alternate Constructors ====================== There are a few alternate class constructors offered. These range from simple convenience wrappers such as :meth:`TOTP.new`, to deserialization methods such as :meth:`TOTP.from_source`. .. automethod:: TOTP.new() .. automethod:: TOTP.from_source .. automethod:: TOTP.from_uri .. automethod:: TOTP.from_json .. automethod:: TOTP.from_dict Factory Creation ================ One powerful method offered by the TOTP class is :meth:`TOTP.using`. This method allows you to quickly create TOTP subclasses with preconfigured defaults, for configuration application secrets and setting default TOTP behavior for your application: .. automethod:: TOTP.using .. _totp-configuration-attributes: Basic Attributes ================ All the TOTP objects offer the following attributes, which correspond to the constructor options above. Most of this information will be serialized by :meth:`TOTP.to_uri` and :meth:`TOTP.to_json`: .. autoattribute:: TOTP.key .. autoattribute:: TOTP.hex_key .. autoattribute:: TOTP.base32_key .. autoattribute:: TOTP.label .. autoattribute:: TOTP.issuer .. autoattribute:: TOTP.digits .. autoattribute:: TOTP.alg .. autoattribute:: TOTP.period .. autoattribute:: TOTP.changed Token Generation ================ Token generation is generally useful client-side, and for generating values to test your server implementation. There is one main generation method: .. automethod:: TOTP.generate .. rst-class:: float-right .. warning:: Tokens should be displayed as strings, as they may contain leading zeros which will get stripped if they are first converted to an :class:`!int`. TotpToken --------- The :meth:`!TOTP.generate` method returns instances of the following class, which offers up detailed information about the generated token: .. autoclass:: TotpToken() Token Matching / Verification ============================= Matching user-provided tokens is the main operation when implementing server-side TOTP support. Passlib offers one main method: :meth:`!TOTP.match`, as well as a convenience wrapper :meth:`!TOTP.verify`: .. automethod:: TOTP.match .. automethod:: TOTP.verify .. seealso:: :ref:`totp-verifying` tutorial for a usage example TotpMatch --------- If successful, the :meth:`!TOTP.verify` method returns instances of the following class, which offers up detailed information about the matched token: .. autoclass:: TotpMatch() .. _totp-provisioning: Client Configuration Methods ============================ Once a server has generated a new TOTP key & configuration, it needs to be communicated to the user in order for them to store it in a suitable TOTP client. This can be done by displaying the key & configuration for the user to hand-enter into their client, or by encoding TOTP object into a URI [#uriformat]_. These configuration URIs can subsequently be displayed as a QR code, for easy transfer to many smartphone-based TOTP clients (such as Authy or Google Authenticator). .. automethod:: TOTP.to_uri .. automethod:: TOTP.pretty_key .. seealso:: * The :meth:`TOTP.from_source` and :meth:`TOTP.from_uri` constructors for decoding URIs. * The :ref:`totp-configuring-clients` tutorial for details about these methods, and how to render URIs to a QR Code. .. _totp-serialization: Serialization Methods ===================== The :meth:`TOTP.to_uri` method is useful, but limited, because it requires additional information (label & issuer), and lacks the ability to encrypt the key. The :class:`TOTP` provides the following methods for serializing TOTP objects to internal storage. When application secrets are configured via :meth:`TOTP.using`, these methods will automatically encrypt the resulting keys. .. automethod:: TOTP.to_json .. automethod:: TOTP.to_dict .. seealso:: * The :meth:`TOTP.from_source` and :meth:`TOTP.from_json` constructors for decoding the results of these methods. * The :ref:`totp-storing-instances` tutorial for more details. Helper Methods ============== While :meth:`TOTP.generate`, :meth:`TOTP.match`, and :meth:`TOTP.verify` automatically handle normalizing tokens & time values, the following methods are exposed in case they are useful in other contexts: .. automethod:: TOTP.normalize_token .. automethod:: TOTP.normalize_time AppWallet ========= The :class:`!AppWallet` class is used internally by the :meth:`TOTP.using` method to store the application secrets provided for handling encrypted keys. If needed, they can also be created and passed in directly. .. autoclass:: AppWallet Support Functions ================= .. autofunction:: generate_secret(entropy=256) Deviations ========== * The TOTP Spec [#totpspec]_ includes an param (``T0``) providing an optional offset from the base time. Passlib omits this parameter (fixing it at ``0``), but so do pretty much all other TOTP implementations. .. rubric:: Footnotes .. [#totpspec] TOTP Specification - :rfc:`6238` .. [#hotpspec] HOTP Specification - :rfc:`4226` .. [#uriformat] Google's OTPAuth URI format - ``_ passlib-1.7.1/docs/lib/passlib.hash.ldap_std.rst0000644000175000017500000001013613016611237022704 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 >>> # hash password >>> hash = lsm.hash("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.7.1/docs/lib/passlib.ext.django.rst0000644000175000017500000001657313015205366022245 0ustar biscuitbiscuit00000000000000.. index:: Django; password hashing plugin .. module:: passlib.ext.django ========================================================== :mod:`passlib.ext.django` - Django Password Hashing Plugin ========================================================== .. versionadded:: 1.6 .. versionchanged:: 1.7 As of Passlib 1.7, this module requires Django 1.8 or newer. .. rst-class:: float-center without-title .. warning:: This extension is a high maintenance, with an uncertain number of users. The current plan is to split this out as a separate package concurrent with Passlib 1.8, and then judge whether it should continue to be maintained in it's own right. See :issue:`81`. 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 1.8 - 1.9. (Support for Django 1.0 - 1.7 was dropped after Passlib 1.6). 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. (Do not replace the reference with another CryptContext, it will break things; just update the context in-place). .. function:: context_changed If the context is modified after loading, call this function to clear internal caches. .. 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.7.1/docs/lib/passlib.hash.bcrypt_sha256.rst0000644000175000017500000000565713016611237023521 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, hash password >>> h = bcrypt_sha256.hash("password") >>> h '$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO' >>> # the same, but with an explicit number of rounds >>> bcrypt.using(rounds=8).hash("password") '$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.7.1/docs/lib/passlib.ifc.rst0000644000175000017500000006556013016611237020744 0ustar biscuitbiscuit00000000000000.. index:: single: PasswordHash interface single: custom hash handler; requirements .. module:: passlib.ifc :synopsis: abstract interfaces used by Passlib ============================================= :mod:`passlib.ifc` -- Password Hash Interface ============================================= .. _password-hash-api: PasswordHash API ================ This module provides the :class:`!PasswordHash` abstract base class. This class defines the common methods and attributes present on all the hashes importable from the :mod:`passlib.hash` module. Additionally, the :class:`passlib.context.CryptContext` class is deliberately designed to parallel many of this interface's methods. .. rst-class:: float-center .. seealso:: :ref:`hash-tutorial` -- Overview of this interface and how to use it. Base Abstract Class =================== .. class:: PasswordHash() This class provides an abstract interface for an arbitrary password hasher. Applications will generally not construct instances directly -- most of the operations are performed via classmethods, allowing instances of a given class to be an internal detail used to implement the various operations. While :class:`!PasswordHash` offers a number of methods and attributes, most applications will only need the two primary methods: * :meth:`PasswordHash.hash` - generate new salt, return hash of password. * :meth:`PasswordHash.verify` - verify password against existing hash. Two additional support methods are also provided: * :meth:`PasswordHash.using` - create subclass with customized configuration. * :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. .. seealso:: :ref:`hash-tutorial` -- Overview of this interface and how to use it. .. _primary-methods: Hashing & Verification Methods ============================== Most applications will only need to use two methods: :meth:`PasswordHash.hash` 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.hash(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`. .. deprecated:: 1.7 Passing :attr:`PasswordHash.setting_kwds` such as ``rounds`` and ``salt_size`` directly into the :meth:`hash` method is deprecated. Callers should instead use ``handler.using(**settings).hash(secret)``. Support for the old method is is tentatively scheduled for removal in Passlib 2.0. Context keywords such as ``user`` should still be provided to :meth:`!hash`. :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. .. 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). .. versionchanged:: 1.7 This method was renamed from :meth:`encrypt`. Deprecated support for passing settings directly into :meth:`!hash`. .. classmethod:: PasswordHash.encrypt(secret, \*\*kwds) Legacy alias for :meth:`PasswordHash.hash`. .. deprecated:: 1.7 This method was renamed to :meth:`!hash` in version 1.7. This alias will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.6. .. 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:`PasswordHash.hash`. 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.hash` 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.hash`). * 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. .. seealso:: * :ref:`hash-verifying` tutorial for a usage example .. _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.hash` much more useful, as it combines the functionality of these two methods into one. .. classmethod:: PasswordHash.genconfig(\*\*setting_kwds) .. deprecated:: 1.7 As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. For all known real-world uses, ``.hash("", **settings)`` should provide equivalent functionality. This deprecation may be reversed if a use-case presents itself in the mean time. Returns a configuration string encoding settings for hash generation. This function takes in all the same :attr:`PasswordHash.setting_kwds` as :meth:`PasswordHash.hash`, 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`). :raises ValueError, TypeError: This function raises exceptions for the same reasons as :meth:`PasswordHash.hash`. .. versionchanged:: 1.7 This should now always return a full hash string, even in cases where previous releases would return a truncated "configuration only" string, or ``None``. .. classmethod:: PasswordHash.genhash(secret, config, \*\*context_kwds) Encrypt secret using specified configuration string. .. deprecated:: 1.7 As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. This deprecation may be reversed if a use-case presents itself in the mean time. 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 :arg config: configuration string to use when hashing the 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`. .. versionchanged:: 1.7 ``None`` is no longer accepted for hashes which (prior to 1.7) lacked a configuration string format. :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.hash` 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.hash`. .. 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: Factory Creation ================ One powerful method offered by the :class:`!PasswordHash` class :meth:`PasswordHash.using`. This method allows you to quickly create subclasses of a specific hash, providing it with preconfigured defaults specific to your application: .. classmethod:: PasswordHash.using(relaxed=False, \*\*settings) This method takes in a set of algorithm-specific settings, and returns a new handler object which uses the specified default settings instead. :param \*\*settings: All 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 ``rounds`` and ``salt_size``. :returns: A new object which adheres to :class:`!PasswordHash` api. :raises ValueError: * If a keywords's value is invalid (e.g. if a ``salt`` string is too small, or a ``rounds`` value is out of range). :raises TypeError: * if a ``kwd`` argument has an incorrect type. .. versionadded:: 1.7 .. seealso:: :ref:`hash-configuring` tutorial for a usage example Hash Inspection Methods ======================= There are currently two hash inspection methods, :meth:`PasswordHash.identify` and :meth:`PasswordHash.needs_update`. .. 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. .. automethod:: PasswordHash.needs_update .. the undocumented and experimental support methods currently include parsehash() and bitsize() .. todo:: document the :attr:`is_disabled` and DisabledHash interface added in passlib 1.7. .. _informational-attributes: .. _general-attributes: General Informational Attributes ================================ 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.using` control hash generation, and which will be encoded into the resulting hash. (These keywords will also be accepted by :meth:`PasswordHash.hash` and :meth:`PasswordHash.genconfig`, though that behavior is deprecated as of Passlib 1.7; and will be removed in Passlib 2.0). 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. ``truncate_error`` This will be present if and only if the hash truncates passwords larger than some limit (reported via it's :attr:`truncate_size` attribute). By default, they will silently truncate passwords above their limit. Setting ``truncate_error=True`` will cause :meth:`PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. index:: single: relaxed; PasswordHash keyword .. _relaxed-keyword: ``relaxed`` By default, passing an invalid value to :meth:`PasswordHash.using` 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. .. versionadded:: 1.6 .. _context-keywords: .. attribute:: PasswordHash.context_kwds Tuple listing the keywords supported by :meth:`PasswordHash.hash`, :meth:`PasswordHash.verify`, and :meth:`PasswordHash.genhash`. These keywords are different from the settings kwds in that the context keywords 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. *Most hash algorithms in Passlib will have no context keywords.* 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. .. attribute:: truncate_size A positive integer, indicating the hash will truncate any passwords larger than this many bytes. If ``None`` (the more common case), indicates the hash will use the entire password provided. Hashes which specify this setting will also support a ``truncate_error`` flag via their :meth:`PasswordHash.using` method, to configure how truncation is handled. .. seealso:: :ref:`hash-configuring` tutorial for a usage example .. _salt-attributes: Salt Information Attributes =========================== 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.binary.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 Attributes ============================= 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.hash`. 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:: document the additional :meth:`PasswordHash.using` keywords available for setting rounds limits. .. 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:`PasswordHash.hash` and :meth:`PasswordHash.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. passlib-1.7.1/docs/lib/passlib.hash.cisco_type7.rst0000644000175000017500000001104013015205366023336 0ustar biscuitbiscuit00000000000000.. index:: Cisco; Type 7 hash ================================================================== :class:`passlib.hash.cisco_type7` - Cisco "Type 7" hash ================================================================== .. danger:: This is not a hash, this is a reversible plaintext encoding. **This format can be trivially decoded**. .. versionadded:: 1.6 .. 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.hash("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.7.1/docs/lib/passlib.context.rst0000644000175000017500000005012013043457152021655 0ustar biscuitbiscuit00000000000000.. index:: CryptContext; reference .. module:: passlib.context :synopsis: CryptContext class, for managing multiple password hash schemes .. _context-reference: ====================================================== :mod:`passlib.context` - CryptContext Hash Manager ====================================================== This page provides a complete reference of all the methods and options supported by the :class:`!CryptContext` class and helper utilities. .. seealso:: * :ref:`context-tutorial` -- overview of this class and walkthrough of how to use it. .. rst-class:: emphasize-children toc-always-open The CryptContext Class ====================== .. class:: CryptContext(schemes=None, \*\*kwds) Helper for hashing 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 toc-always-open 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.hash` 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 creating 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.hash` method:: >>> from passlib.context import CryptContext >>> myctx = CryptContext(schemes=["sha256_crypt", "md5_crypt"]) >>> # hash() uses the first scheme >>> myctx.default_scheme() 'sha256_crypt' >>> myctx.hash("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.hash("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 :samp:`truncate_error` By default, some algorithms will truncate large passwords (e.g. :class:`~passlib.hash.bcrypt` truncates ones larger than 72 bytes). Such hashes accept a ``truncate_error=True`` option to make them raise a :exc:`~passlib.exc.PasswordTruncateError` instead. This can also be set at the CryptContext level, and will passed to all hashes that support it. .. versionadded:: 1.7 .. _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, is ignored by 1.7, and will be removed in version 1.8. .. versionchanged:: 1.7 Per deprecation roadmap above, this option is now ignored. .. _context-harden-verify-option: ``harden_verify`` Companion to ``min_verify_time``, currently ignored. .. versionadded:: 1.7 .. deprecated:: 1.7.1 This option is ignored by 1.7.1, and will be removed in 1.8 along with ``min_verify_time``. .. _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}__rounds` Set the number of rounds required for this scheme when generating new hashes (using :meth:`~CryptContext.hash`). Existing hashes which have a different number of rounds will be marked as deprecated. This essentially sets ``default_rounds``, ``min_rounds``, and ``max_rounds`` all at once. If any of those options are also specified, they will override the value specified by ``rounds``. .. versionadded:: 1.7 Previous releases of Passlib treated this as an alias for ``default_rounds``. :samp:`{scheme}__default_rounds` Sets the default number of rounds to use with this scheme when generating new hashes (using :meth:`~CryptContext.hash`). 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 hash() uses sha256_crypt's default (80000) >>> myctx = CryptContext(["sha256_crypt"]) >>> myctx.hash("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.hash("fooey") '$5$rounds=77123$60Y7mpmAhUv6RDvj$AdseAOq6bKUZRDRTr/2QK1t38qm3P6sYeXhXKnBAmg0' ^^^^^ .. seealso:: the :ref:`context-default-settings-example` example in the tutorial. :samp:`{scheme}__vary_rounds` .. deprecated:: 1.7 This option has been deprecated as of Passlib 1.7, and will be removed in Passlib 2.0. The (very minimal) security benefit it provides was judged to not be worth code complexity it requires. Instead of using a fixed rounds value (such as specified by ``default_rounds``, above); this option will cause each call to :meth:`~CryptContext.hash` 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, hash() uses the same amount each time: >>> from passlib.context import CryptContext >>> myctx = CryptContext(schemes=["sha256_crypt"], ... sha256_crypt__default_rounds=80000) >>> myctx.hash("fooey") '$5$rounds=80000$60Y7mpmAhUv6RDvj$AdseAOq6bKUZRDRTr/2QK1t38qm3P6sYeXhXKnBAmg0' >>> myctx.hash("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.hash("fooey") '$5$rounds=83966$bMpgQxN2hXo2kVr4$jL4Q3ov41UPgSbO7jYL0PdtsOg5koo4mCa.UEF3zan.' >>> myctx.hash("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:`!hash` :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. .. deprecated:: 1.7 This special scheme is deprecated as of Passlib 1.7, and will be removed in Passlib 2.0. It's only legitimate use was for ``vary_rounds``, which is also being removed in Passlib 2.0. .. _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 hash() with ``category=None`` would result >>> # in a hash that used 77000 sha256-crypt rounds: >>> myctx.hash("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.hash("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.hash .. automethod:: CryptContext.encrypt .. automethod:: CryptContext.verify .. automethod:: CryptContext.identify .. automethod:: CryptContext.dummy_verify .. 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 regenerate 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 .. _context-disabled-hashes: Disabled Hash Managment ----------------------- .. versionadded:: 1.7 It's frequently useful to disable a user's ability to login by replacing their password hash with a standin that's guaranteed to never verify, against *any* password. CryptContext offers some convenience methods for this through the following API. .. automethod:: CryptContext.disable .. automethod:: CryptContext.enable .. automethod:: CryptContext.is_enabled 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 .. autoattribute:: CryptContext.context_kwds .. 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.7.1/docs/lib/passlib.hash.plaintext.rst0000644000175000017500000000216313015205366023124 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.hash("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.7.1/docs/lib/passlib.hash.crypt16.rst0000644000175000017500000001072413015205366022426 0ustar biscuitbiscuit00000000000000======================================================================= :class:`passlib.hash.crypt16` - Crypt16 ======================================================================= .. include:: ../_fragments/trivial_hash_warning.rst .. 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.7.1/docs/lib/passlib.registry.rst0000644000175000017500000000537612257351267022064 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.7.1/docs/lib/passlib.hash.fshp.rst0000644000175000017500000001160213016611237022051 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, hash password >>> hash = fshp.hash("password") >>> hash '{FSHP1|16|16384}PtoqcGUetmVEy/uR8715TNqKa8+teMF9qZO1lA9lJNUm1EQBLPZ+qPRLeEPHqy6C' >>> # the same, but with an explicit number of rounds, larger salt, and specific variant >>> fshp.using(rounds=40000, salt_size=32, variant="sha512").hash("password") '{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.crypto.digest.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.7.1/docs/lib/passlib.hash.lmhash.rst0000644000175000017500000001476413016611237022401 0ustar biscuitbiscuit00000000000000.. index:: LAN Manager hash, Windows; LAN Manager hash ================================================================== :class:`passlib.hash.lmhash` - LanManager Hash ================================================================== .. include:: ../_fragments/insecure_hash_warning.rst .. versionadded:: 1.6 .. 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 >>> # hash password >>> h = lmhash.hash("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 it's default encoding for unicode strings. However, if your database used a different encoding, you will need to either first encode the passwords into bytes, or override the default encoding via ``lmhash.hash(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.7.1/docs/lib/passlib.hash.grub_pbkdf2_sha512.rst0000644000175000017500000000622513015205366024371 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.crypto.digest.pbkdf1` 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.hash("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.7.1/docs/lib/passlib.utils.des.rst0000644000175000017500000000151613015205366022105 0ustar biscuitbiscuit00000000000000==================================================== :mod:`passlib.utils.des` - DES routines [deprecated] ==================================================== .. module:: passlib.utils.des :synopsis: routines for performing DES encryption .. warning:: This module is deprecated as of Passlib 1.7: It has been relocated to :mod:`passlib.crypto.des`; and the aliases here will be removed in Passlib 2.0. 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.7.1/docs/requirements.txt0000644000175000017500000000036513015205366020524 0ustar biscuitbiscuit00000000000000# NOTE: switched to custom branch until https://github.com/dreamhost/sphinxcontrib-fulltoc/issues/10 is fixed ## sphinxcontrib-fulltoc git+https://github.com/eli-collins/sphinxcontrib-fulltoc.git hg+https://bitbucket.org/ecollins/cloud_sptheme passlib-1.7.1/docs/install.rst0000644000175000017500000001232313043770000017426 0ustar biscuitbiscuit00000000000000============ Installation ============ .. index:: Google App Engine; compatibility Supported Platforms =================== Passlib requires Python 2 (>= 2.6) or Python 3 (>= 3.3). It is known to work with the following Python implementations: * CPython 2 -- v2.6 or newer. * CPython 3 -- v3.3 or newer. * PyPy -- v2.0 or newer. * PyPy3 -- v5.3 or newer. * Jython -- v2.7 or newer. * Pyston -- v0.5.1 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. .. versionchanged:: 1.7 Support for Python 2.5, 3.0-3.2 was dropped. Support for PyPy 1.x was dropped. .. _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. Use ``pip install passlib[bcrypt]`` to get the recommended bcrypt setup. * `argon2_cffi `_, or `argon2pure `_ (>= 1.2.2) If any of these packages are installed, they will be used to provide support for the :class:`~passlib.hash.argon2` hash algorithm. `argon2_cffi `_ is currently the recommended option. Use ``pip install passlib[argon2]`` to get the recommended argon2 setup. * `Cryptography `_ If installed, will be used to enable encryption of TOTP secrets for storage (see :mod:`passlib.totp`). Use ``pip install passlib[totp]`` to get the recommended TOTP setup. * `fastpbk2 `_ If installed, will be used to greatly speed up :func:`~passlib.crypto.digest.pbkdf2_hmac`, and any pbkdf2-based hashes. * `SCrypt `_ (>= 0.6) If installed, this will be used to provide support for the :class:`~passlib.hash.scrypt` hash algorithm. If not installed, a MUCH slower builtin reference implementation will be used. .. versionchanged:: 1.7 Added fastpbkdf2, cryptography, argon2_cffi, argon2pure, and scrypt support. Removed M2Crypto support. Installation Instructions ========================= To install from PyPi using :command:`pip`:: pip install passlib .. As noted above, you can ensure you have feature-specific extras installed via any of:: pip install passlib[argon2] pip install passlib[bcrypt] pip install passlib[totp] 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 (as well as the ``unittest2`` library under Python 2.6). Once Passlib and Nose have been installed, the main suite of tests may be run using:: nosetests --tests passlib.tests By default, this runs the main battery of tests, but omits some additional ones (such as internal cross-checks, and mock-testing of features not provided natively by the host OS). To run these tests as well, set the following environmental variable:: PASSLIB_TEST_MODE="full" nosetests --tests passlib.tests To run a quick check to confirm just basic functionality, with a pared-down set of tests:: PASSLIB_TEST_MODE="quick" nosetests --tests passlib.tests Tests may also be run via ``setup.py test`` or the included ``tox.ini`` file. The ``tox.ini`` file is used to test passlib before each release, and contains a number different environment setups. These tests require `tox `_ 2.5 or later. .. 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.4 or newer) 2. Install the `Cloud Sphinx Theme `_ (1.8.2 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. passlib-1.7.1/docs/narr/0000755000175000017500000000000013043774617016211 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/narr/index.rst0000644000175000017500000000112313015205366020034 0ustar biscuitbiscuit00000000000000======================= Walkthrough & Tutorials ======================= .. xxx: 'introductory materials' etc aren't proper sections so that sphinx TOC in sidebar will show all walkthroughs no matter which of them user is currently in **Introductory Materials** .. rst-class:: html-highlight-pages space-pages .. toctree:: :maxdepth: 2 /install overview quickstart **Tutorials** .. rst-class:: html-highlight-pages space-pages .. toctree:: :maxdepth: 2 hash-tutorial context-tutorial totp-tutorial passlib-1.7.1/docs/narr/context-tutorial.rst0000644000175000017500000005107613043457152022271 0ustar biscuitbiscuit00000000000000.. index:: CryptContext; overview .. _context-tutorial: .. currentmodule:: passlib.context =============================================== :class:`~passlib.context.CryptContext` Tutorial =============================================== 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. .. rst-class:: float-center .. seealso:: The :mod:`passlib.context` api reference, which lists all the options and methods supported by this class. .. index:: CryptContext; usage examples .. rst-class:: emphasize-children Walkthrough Outline =================== * `Basic Usage`_ * `Using Default Settings`_ * `Loading & Saving a CryptContext`_ * `Deprecation & Hash Migration`_ * `Full Integration Example`_ .. todo:: This tutorial doesn't yet cover 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 collection 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.hash("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.hash("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 creating 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.hash("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.hash`, :meth:`~CryptContext.verify`, and :meth:`~CryptContext.identify` methods. * the :ref:`schemes ` and :ref:`default ` constructor options. .. _context-default-settings-example: Using Default Settings ====================== While creating 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:`!hash` 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.hash("password", scheme="sha256_crypt") '$5$rounds=80000$GgU/gwNBs9SaObqs$ohY23/zm.8O0TpkGx5fxk0aeVdFpaeKo9GUkMJ0VrMC' ^^^^^ >>> # ldap_salted_md5 with an 8 byte salt... >>> myctx.hash("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.hash("password", scheme="sha256_crypt") '$5$rounds=91234$GgU/gwNBs9SaObqs$ohY23/zm.8O0TpkGx5fxk0aeVdFpaeKo9GUkMJ0VrMC' ^^^^^ >>> myctx.hash("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.hash(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.hash("password") '$5$rounds=131072$i6xuFK6j8r66ahGn$r.7H8HUk30qiH7fIWRJFJfhWG925nRZh90aYPMdewr3' ^^^^^^ >>> # hashes with enough rounds won't show up as deprecated... >>> myctx.needs_update(hash1) False 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. Undocumented Features ===================== .. todo:: Document usage of the :ref:`context-disabled-hashes` options. .. 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 hashing new passwords. pbkdf2_sha1__default_rounds = 15000 ; 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.hash(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 # # NOTE: if the user account is missing, or has no hash, # you can pass ``hash=None`` to verify_and_update() # mask this from the attacker by simulating the delay # a real verification would have taken. # hash=None will never verify. 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.7.1/docs/narr/hash-tutorial.rst0000644000175000017500000002725413016613110021515 0ustar biscuitbiscuit00000000000000.. index:: single: PasswordHash interface single: custom hash handler; requirements .. _hash-tutorial: .. currentmodule:: passlib.ifc =========================================== :class:`~passlib.ifc.PasswordHash` Tutorial =========================================== Overview ======== Passlib supports a large number of hash algorithms, all of which can be imported from the :mod:`passlib.hash` module. While the exact options and behavior will vary between each algorithm, all of the hashes provided by Passlib use the same interface, defined by the :class:`passlib.ifc.PasswordHash` abstract class. The :class:`!PasswordHash` class provides a generic interface for interacting individually with the various hashing algorithms. It offers methods and attributes for a number of use-cases, which fall into three general categories: * Creating & verifying hashes * Examining the configuration of a hasher, and customizing the defaults. * Assorting supplementary methods. .. seealso:: * :mod:`passlib.ifc` -- API reference of all the methods and attributes of the :class:`!PasswordHash` class. * :ref:`passlib.context.CryptContext ` -- For working with multiple hash formats at once (such a user account table with multiple existing hash formats). .. _hash-verifying: .. _password-hash-examples: Hashing & Verifying =================== While all the hashers in :mod:`passlib.hash` offer a range of methods and attributes, the main activities applications will need to perform is hashing and verifying passwords. This can be done with the :meth:`PasswordHash.hash` and :meth:`PasswordHash.verify` methods. .. rst-class:: float-center without-title .. caution:: **Changed in 1.7:** Prior releases used :meth:`PasswordHash.encrypt` for hashing, which has now been renamed to :meth:`PasswordHash.hash`. A compatibility alias is present in 1.7, but will be removed in Passlib 2.0. Hashing ------- First, import the desired hash. The following example uses the :class:`~passlib.hash.pbkdf2_sha256` class (which derives from :class:`!PasswordHash`):: >>> # import the desired hasher >>> from passlib.hash import pbkdf2_sha256 Use :meth:`PasswordHash.hash` to hash a password. This call takes care of unicode encoding, picking default rounds values, and generating a random salt:: >>> hash = pbkdf2_sha256.hash("password") >>> hash '$pbkdf2-sha256$29000$9t7be09prfXee2/NOUeotQ$Y.RDnnq8vsezSZSKy1QNy6xhKPdoBIwc.0XDdRm9sJ8' Note that since each call generates a new salt, the contents of the resulting hash will differ between calls (despite using the same password as input):: >>> hash2 = pbkdf2_sha256.hash("password") >>> hash2 '$pbkdf2-sha256$29000$V0rJeS.FcO4dw/h/D6E0Bg$FyLs7omUppxzXkARJQSl.ozcEOhgp3tNgNsKIAhKmp8' ^^^^^^^^^^^^^^^^^^^^^^ Verifying --------- Subsequently, you can call :meth:`PasswordHash.verify` to check user input against an existing hash:: >>> pbkdf2_sha256.verify("password", hash) True >>> pbkdf2_sha256.verify("joshua", hash) False .. _hash-unicode-behavior: Unicode & non-ASCII Characters ------------------------------ *Sidenote 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`). One exception is legacy hashes that were generated using a different character encoding. In this case, passwords should be encoded using the correct encoding before they are passed to :meth:`!verify`; otherwise users may not be able to log in successfully. For proper internationalization, applications should also take care to ensure unicode inputs are normalized to a single representation before hashing. The :func:`passlib.utils.saslprep` function can be used for this purpose. .. _hash-configuring: Customizing the Configuration ============================= The using() Method ------------------ Each hasher contains a number of :ref:`informational attributes `. many of which can be customized to change the properties of the hashes generated by :meth:`PasswordHash.hash`. When you want to change the defaults, you don't have to modify the hasher class directly, or pass in the options to each call to :meth:`!PasswordHash.hash`. Instead, all the hashes offer a :meth:`PasswordHash.using` method. This is a powerful method which accepts most hash informational attributes, as well as some other hash-specific configuration keywords; and returns a subclass of the original hasher (or a object with an identical interface). The returned object inherits the defaults settings from it's parent, but integrates any values you choose to override. .. rst-class:: float-center without-title .. caution:: **Changed in 1.7:** Prior releases required you to pass custom settings to each :meth:`PasswordHash.encrypt` call. That usage pattern is deprecated, and will be removed in Passlib 2.0; code should be switched to use :meth:`PasswordHash.using`, as shown below. Usage Example ------------- As an example, if the hasher you select supports a variable number of iterations (such as :class:`~passlib.hash.pbkdf2_sha256`), you can specify a custom value using the ``rounds`` keyword. Here, the default class uses 29000 rounds:: >>> from passlib.hash import pbkdf2_sha256 >>> pbkdf2_sha256.default_rounds 29000 >>> pbkdf2_sha256.hash("password") '$pbkdf2-sha256$29000$V0rJeS.FcO4dw/h/D6E0Bg$FyLs7omUppxzXkARJQSl.ozcEOhgp3tNgNsKIAhKmp8' ^^^^^ But if we call :meth:`PasswordHash.using`, we can override this value:: >>> custom_pbkdf2 = pbkdf2_sha256.using(rounds=123456) >>> custom_pbkdf2.default_rounds 123456 >>> custom_pbkdf2.hash("password") '$pbkdf2-sha256$123456$QwjBmJPSOsf4HyNE6L239g$8m1pnP69EYeOiKKb5sNSiYw9M8pJMyeW.CSm0KKO.GI' ^^^^^^ Other Keywords -------------- While hashes frequently have additional keywords supported by using, the basic set of settings you can customize can be found by inspecting the :attr:`PasswordHash.setting_kwds` attribute:: >>> pbkdf2_sha256.settings_kwds ("salt", "salt_size", "rounds") For instance, the following generates pbkdf2 hashes with a 32-byte salt instead of the default 16:: >>> pbkdf2_sha256.using(salt_size=8).hash("password") '$pbkdf2-sha256$29000$tPZ.r5UyZgyhNEaI8Z5z7r1X6p1zTknJ.T/nHINwbq0$RlM49Qf5qRraHx.L7gq3hKIKSMLttrG1zWmWXyfXqc8' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This method is also used internally by the :ref:`CryptContext ` class it order to create a custom hasher configured based on the CryptContext policy it was provided. .. seealso:: * :meth:`PasswordHash.using` -- API reference Context Keywords ================ While the :meth:`PasswordHash.hash` example above 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` hash, where hashing requires a username:: >>> from passlib.hash import oracle10 >>> hash = oracle10.hash("secret", user="admin") 'B858CE295C95193F' The difference between this and specifying something like a rounds setting (see :ref:`hash-configuring` above) is that a configuration option only needs to be specified once, and is then encoded into the hash string itself... Whereas a context keyword represents something that isn't stored in the hash string, and needs to be specified every time you call :meth:`PasswordHash.hash` **or** :meth:`PasswordHash.verify`:: >>> oracle10.verify("secret", hash, user="admin") True In this example, 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 a context keywords when it's required will cause a TypeError:: >>> hash = oracle10.hash("password") Traceback (most recent call last): TypeError: user must be unicode or bytes, not None 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:: >>> oracle10.context_kwds ("user",) >>> pbkdf2_sha256.context_kwds () Identifying Hashes ================== One of the rarer use-cases is the need to identify whether a string recognizably belongs to a given hasher class. This can be important in some cases, because attempting to call :meth:`PasswordHash.verify` with another algorithm's hash will result in a ValueError:: >>> from passlib.hash import pbkdf2_sha256, md5_crypt >>> other_hash = md5_crypt.hash("password") >>> pbkdf2_sha256.verify("password", other_hash) Traceback (most recent call last): ValueError: not a valid pbkdf2_sha256 hash This can be prevented by using the identify method, which determines whether a hash belongs to a given algorithm:: >>> hash = pbkdf2_sha256.hash("password") >>> pbkdf2_sha256.identify(hash) True >>> pbkdf2_sha256.identify(other_hash) False .. rst-class:: float-center .. seealso:: In most cases where an application needs to distinguish between multiple hash formats, it will be more useful to switch to a :ref:`CryptContext ` object, which automatically handles this and many similar tasks. .. todo:: Document usage of :meth:`PasswordHash.needs_update`, and how it ties into :meth:`PasswordHash.using`. .. 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... .. centered:: 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 "more 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, and to use one of :ref:`recommended hashes `, as one of their chief qualifying features is the mere *existence* of rounds values which take a short enough amount of time, and yet are still considered secure. .. todo:: Expand this section into a full document, including information from the following posts: * http://stackoverflow.com/questions/13545677/python-passlib-what-is-the-best-value-for-rounds * http://stackoverflow.com/questions/11829602/pbkdf2-and-hash-comparison As well as maybe JS-interactive calculation helper. .. [#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.7.1/docs/narr/quickstart.rst0000644000175000017500000003132513041200316021113 0ustar biscuitbiscuit00000000000000================================ New Application Quickstart Guide ================================ Need to quickly get password hash support added into your new application, but don't have time to wade through pages of documentation, comparing and contrasting all the different schemes? Then read on... .. NOTE: commented this out for now, considering deprecating the "custom_app_context", since it's hard to convey policy changes to users. May reenable if decide this is still good route to go. 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 >>> # hash a password... >>> hash = pwd_context.hash("somepass") >>> # verifying a password... >>> ok = pwd_context.verify("somepass", hash) >>> # [optional] hashing 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.hash("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: .. rst-class:: toc-always-open Choosing a Hash ================ 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. .. rst-class:: float-center without-title .. seealso:: If you already know what hash algorithm(s) you want to use, skip to the next section: `Creating and Using a CryptContext`_. The Options ----------- There are currently four good choices [#choices]_ for secure hashing: * :class:`~passlib.hash.argon2` * :class:`~passlib.hash.bcrypt` * :class:`~passlib.hash.pbkdf2_sha256` / :class:`~passlib.hash.pbkdf2_sha512` * :class:`~passlib.hash.sha256_crypt` / :class:`~passlib.hash.sha512_crypt` All four hashes share the following properties: * No known vulnerabilities. * Based on documented & widely reviewed algorithms. * Public-domain or BSD-licensed reference implementations available. * variable rounds for configuring flexible cpu cost on a per-hash basis. * At least 96 bits of salt. * Basic algorithm has seen heavy scrutiny and use for at least 10 years *(except for Argon2, born around 2013)*. * In use across a number of OSes and/or a wide variety of applications. While Argon2 is much younger than the others, it has seen heavy scrutiny, and was purpose-designed for password hashing. In the near future, it stands likely to become *the* recommended standard. .. rst-class:: html-toggle Detailed Comparison of Choices ------------------------------ Argon2 ...... :class:`~passlib.hash.argon2` is the newest of the four recommended hashes. It was selected as the winner of the `2013 Password Hashing Competition `_, and draws on the design and lessons from BCrypt, PBKDF2, and SCrypt. Despite being much newer than the others, it has seen heavy scrutiny. Since the Argon2 project had the foresight to provide not just a reference implementation, but a standard hash encoding format, these hashes should be reliably interoperatable across all implementations. *Issues:* In it's default configuration, Argon2 uses more memory than the other hashes. However, this is one of it's hallmarks as a "memory hard" hashing algorithm, and contributes to it's security. Furthermore the exact amount used is configurable. It's only main drawback is that as of 2016-6-20 it's only 3 years old. It's seen only a few minor adjustments since 2013, but as it is just now gaining widespread use, the next few years are the period in which it will likely either prove itself, or be found wanting. It's for this reason, any cryptographic algorithm less than a decade old is generally considered "young" :) 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). Finally, BCrypt only hashes the first 72 characters of a password, and will silently truncate longer ones (Passlib's non-standard :class:`~passlib.hash.bcrypt_sha256` works around this last issue). PBKDF2 ...... :class:`~passlib.hash.pbkdf2_sha512` is a custom hash 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 major security or portability issues, and compares favorably against bcrypt. However, bcrypt has proven slightly more resistant to modern GPU-based cracking techniques. 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 of 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. While this algorithm is still considered secure, it has fallen out of favor in comparison to bcrypt & pbkdf2, due to it's non-standard construction. Furthermore, when compared to Argon2 and BCrypt, SHA512-Crypt and PBKDF2 have proven more susceptible to cracking using modern GPU-based techniques. .. 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. Making a Decision ----------------- For new applications, this decision comes down to a couple of questions: 1. Does the hash need to be natively supported by your operating system's :func:`!crypt` api, in order to allow inter-operation with third-party applications on the host? * If yes, the right choice is either :class:`~passlib.hash.bcrypt` for BSD variants, or :class:`~passlib.hash.sha512_crypt` for Linux; since these are natively supported. * If no, continue... 2. Does your hosting provider allow you to install C extensions? * If no, you probably want to use :class:`~passlib.hash.pbkdf2_sha256`, as this currently has the fastest pure-python backend. * If they allow C extensions, continue... 3. Do you want to use the latest & greatest, and don't mind increased memory usage when hashing? * :class:`~passlib.hash.argon2` is a next-generation hashing algorithm, attempting to become the new standard. It's design has been being slightly tweaked since 2013, but will quite likely become *the* standard in the next few years. You'll need to install the `argon2_cffi `_ support library. * If you want something secure, but more battle tested, continue... 4. The top choices left are :class:`~passlib.hash.bcrypt` and :class:`~passlib.hash.pbkdf2_sha256`. Both have advantages, and their respective rough edges; though currently the balance is in favor of bcrypt (pbkdf2 can be cracked somewhat more efficiently). * If choosing bcrypt, we strongly recommend installing the `bcrypt `_ support library on non-BSD operating systems. * If choosing pbkdf2, especially on python2 < 2.7.8 and python 3 < 3.4, you will probably want to install `fastpbk2 `_ support library. Creating and Using a CryptContext ================================= Once 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 related policy configuration. 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 additional support for reading legacy des_crypt hashes. schemes=["pbkdf2_sha256", "des_crypt"], # Automatically mark all but first hasher in list as deprecated. # (this will be the default in Passlib 2.0) deprecated="auto", # Optionally, 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. # Leaving this alone is usually safe, and will use passlib's defaults. ## pbkdf2_sha256__rounds = 29000, ) 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 >>> # hashing a password... >>> hash = pwd_context.hash("somepass") >>> hash '$pbkdf2-sha256$29000$BSBkLEXIeS9FKMW4F.I85w$SJMzqVU7fw49NDOJZHt2o9vKIfDUVM4cKlAD4MxIgD0' >>> # verifying a password... >>> pwd_context.verify("somepass", hash) True >>> pwd_context.verify("wrongpass", hash) False There's many more features packed into the context objects, read the walkthrough for more... .. rst-class:: float-center .. seealso:: * :ref:`context-tutorial` -- full details of using the CryptContext class * :mod:`passlib.context` -- CryptContext API reference * :mod:`passlib.hash` -- list of all hashes supported by Passlib. .. rubric:: Footnotes .. [#choices] As of June 2016, the most commonly used password hashes are BCrypt and PBKDF2, followed by SHA512-Crypt, with Argon2 rapidly moving up the ranks. You should make sure you are reading a current copy of the Passlib documentation, in case the state of things has changed. passlib-1.7.1/docs/narr/totp-tutorial.rst0000644000175000017500000007620313015205366021567 0ustar biscuitbiscuit00000000000000.. index:: TOTP; overview .. index:: TOTP; usage examples .. _totp-tutorial: .. currentmodule:: passlib.totp ==================================== :class:`~passlib.totp.TOTP` Tutorial ==================================== Overview ======== The :mod:`passlib.totp` module provides a set of classes for adding two-factor authentication (2FA) support into your application, using the widely supported TOTP specification (:rfc:`6238`). This module is based around the :class:`TOTP` class, which supports a wide variety of use-cases, including: * Creating & transferring configured TOTP keys to client devices. * Generating & verifying tokens. * Securely storing configured TOTP keys. .. seealso:: The :mod:`passlib.totp` API reference, which lists all details of all the classes and methods mentioned here. Walkthrough =========== There are a number of different ways to integrate TOTP support into a server application. The following is a general outline of one of way to do this. Some details and alternate choices are omitted for brevity, see the remaining sections of this tutorial for more detailed information about these steps. .. _totp-walkthrough-step-1: 1. Generate an Application Secret --------------------------------- First, generate a strong application secret to use when encrypting TOTP keys for storage. Passlib offers a :meth:`generate_secret` method to help with this:: >>> from passlib.totp import generate_secret >>> generate_secret() 'pO7SwEFcUPvIDeAJr7INBj0TjsSZJr1d2ddsFL9r5eq' This key should be assigned a numeric tag (e.g. "1", a timestamp, or an iso date such as "2016-11-10"); and should be stored in a file *separate* from your application's configuration. Ideally, after this file has been loaded by the TOTP constructor below, the application should give up access permissions to the file. Example file contents:: 2016-11-10: pO7SwEFcUPvIDeAJr7INBj0TjsSZJr1d2ddsFL9r5eq This key will be used in a later step to encrypt TOTP keys for storage in your database. The sequential tag is used so that if your database (or the application secrets) are ever compromised, you can add a new application secret (with a newer tag), and gracefully migrate the compromised TOTP keys. .. rst-class:: without-title float-center .. seealso:: **For more details see** :ref:`totp-encryption-setup` (below). 2. TOTP Factory Initialization ------------------------------ When your application is being initialized, create a TOTP factory which is configured for your application, and is set up to use the application secrets defined in step 1. You can also set a default issuer here, instead of having to provide one explicitly in step 4:: >>> from passlib.totp import TOTP >>> TotpFactory = TOTP.using(secrets_path='/path/to/secret/file/in/step/1', ... issuer="myapp.example.org") The ``TotpFactory`` object returned by :meth:`TOTP.using` is actually a subclass of :class:`TOTP` itself, and has the same methods and attributes. The main difference is that (because an application secret has been provided), the TOTP key will automatically be encrypted / decrypted when serializing the object to disk. .. rst-class:: without-title float-center .. seealso:: **For more details see** :ref:`totp-creation` (below). 3. Rate-Limiting & Cache Initialization --------------------------------------- As part of your application initialization, it **critically important** to set up infrastructure to rate limit how many token verification attempts a user / ip address is allowed to make, otherwise TOTP can be bypassed. .. rst-class:: without-title float-center .. seealso:: **For more details see** :ref:`totp-rate-limiting` (below) .. rst-class:: clear It's also **strongly recommended** to set up a per-user cache which can store the last matched TOTP counter (an integer) for a period of a few minutes (e.g. using `dogpile.cache `_, memcached, redis, etc). This cache is used by later steps to protect your application during a narrow window of time where TOTP would otherwise be vulnerable to a replay attack. .. rst-class:: without-title float-center .. seealso:: **For more details see** :ref:`totp-reuse-warning` (below) 4. Setting up TOTP for a User ----------------------------- To set up TOTP for a new user: create a new TOTP object and key using :meth:`TOTP.new`. This can then be rendered into a provisioning URI, and transferred to the user's TOTP client of choice. Rendering to a provisioning URI using :meth:`TOTP.to_uri` requires picking an "issuer" string to uniquely identify your application, and a "label" string to uniquely identify the user. The following example creates a new TOTP instance with a new key, and renders it to a URI, plugging in application-specific information. Using the ``TotpFactory`` object set up in step 2:: >>> totp = TotpFactory.new() >>> uri = totp.to_uri(issuer="myapp.example.org", label="username") >>> uri 'otpauth://totp/username?secret=D6RZI4ROAUQKJNAWQKYPN7W7LNV43GOT&issuer=myapp.example.org' This URI is generally passed to a QRCode renderer, though as fallback it's recommended to also display the key using :meth:`TOTP.pretty_key`. .. rst-class:: without-title float-center .. seealso:: **For more details, and more about QR Codes, see** :ref:`totp-configuring-clients` (below). 5. Storing the TOTP object -------------------------- Before enabling TOTP for the user's account, it's good practice to first have the user successfully verify a token (per step 6); thus confirming their client h as been correctly configured. Once this is done, you can store the TOTP object in your database. This can be done via the :meth:`TOTP.to_json` method:: >>> totp.to_json() '{"enckey":{"c":14,"k":"FLEQC3VO6SIT3T7GN2GIG6ONPXADG5CZ","s":"UL2J4MZG4SONHOWXLKFQ","t":"1","v":1},"type":"totp","v":1}' Note that if there is no application secret configured, the key will not be encrypted, and instead look like this:: >>> totp.to_json() '{"key":"D6RZI4ROAUQKJNAWQKYPN7W7LNV43GOT","type":"totp","v":1}' To ensure you always save an encrypted token, you can use ``totp.to_json(encrypted=True)``. .. rst-class:: float-center without-title .. seealso:: **For more details see** :ref:`totp-storing-instances` 6. Verifying a Token -------------------- Whenever attempting to verify a token provided by the user, first load the serialized TOTP object from the database (stored step 5), as well as the last counter value from the cache (set up in step 3). You should use these values to call the :meth:`TOTP.verify` method. If verify() succeeds, it will return a :class:`TotpMatch` object. This object contains information about the match, including :attr:`TotpMatch.counter` (a time-dependant integer tied to this token), and :attr:`TotpMatch.cache_seconds` (minimum time this counter should be cached). If verify() fails, it will raise one of the :exc:`passlib.exc.TokenError` subclasses indicating what went wrong. This will be one of three cases: the token was malformed (e.g. too few digits), the token was invalid (didn't match), or a recent token was reused. A skeleton example of how this should function:: >>> from passlib.exc import TokenError, MalformedTokenError >>> # pull information from your application >>> token = # ... token string provided by user ... >>> source = # ... load totp json string from database ... >>> last_counter = # ... load counter value from cache ... >>> # ... check attempt rate limit for this account / address (per step 3 above) ... >>> # using the TotpFactory object defined in step 2, invoke verify >>> try: ... match = TotpFactory.verify(token, source, last_counter=last_counter) ... except MalformedTokenError as err: ... # --- malformed token --- ... # * inform user, e.g. by displaying str(err) ... except TokenError as err: ... # --- invalid or reused token --- ... # * add to rate limit counter ... # * inform user, e.g. by displaying str(err) ... else: ... # --- successful match --- ... # * reset rate-limit counter ... # * store 'match.counter' in per-user cache for at least 'match.cache_seconds' .. rst-class:: float-center without-title .. seealso:: **For more details see** :ref:`totp-verifying` (below) .. rst-class:: html-toggle Alternate Caching Strategy .......................... As an alternative to storing ``match.counter`` in the cache, applications using a cache such as memcached may wish to simply set a key based on ``user + token`` for ``match.cache_seconds``, and reject any tokens coming in for that user who are marked in the cache. In that case, they should run the tokens through :meth:`TOTP.normalize_token` first, to make sure the token strings are normalized before comparison. In this case, the skeleton example can be amended to:: >>> # pull information from your application >>> token = # ... token string provided by user ... >>> source = # ... load totp json string from database ... >>> user_id = # ... user identifier for cache >>> # ... check attempt rate limit for this account / address (per step 3 above) ... >>> # check token format >>> try: ... token = TotpFactory.normalize_token(token) ... except MalformedTokenError as err: ... # --- malformed token --- ... # * inform user, e.g. by displaying str(err) ... return >>> # check if token has been used, using app-defined present_in_cache() helper >>> cache_key = "totp-token-%s-%s" % (user_id, token) >>> if present_in_cache(cache_key): ... # * add to rate limit counter ... # * present 'token already used' message ... return >>> # using the TotpFactory object defined in step 2, invoke verify >>> try: ... match = TotpFactory.verify(token, source) ... except TokenError as err: ... # --- invalid token --- ... # * add to rate limit counter ... # * inform user, e.g. by displaying str(err) ... else: ... # --- successful match --- ... # * reset rate-limit counter ... # * set 'cache_key' in per-user cache for at least 'match.cache_seconds' 7. Reserializing Existing Objects --------------------------------- An organization's security policy may require that a developer periodically change the application secret key used to decrypt/encrypt TOTP objects. Alternately, the application secret may become compromised. In either case, a new application secret will need to be created, and a new tag assigned (per step 1). Any deprecated secret(s) will need to be retained in the collection passed to the ``TotpFactory``, in order to be able to decrypt existing TOTP objects. .. rst-class:: float-right .. note:: You can verify which secret is will be used to encrypt new keys by inspecting ``tag = TotpFactory.wallet.default_tag``. .. rst-class:: clear Once the new secret has been added, you will need to update all the serialized TOTP objects in the database, decrypting them using the old secret, and encrypting them with the new one. This can be done in a few ways. The following skeleton example gives a simple loop that can be used, which would ideally be run in a process that's separate from your normal application:: >>> # presuming query_user_totp() queries your database for all user rows, >>> # and update_user_totp() updates a specific row. >>> for user_id, totp_source in query_user_totp(): >>> totp = TotpFactory.from_source(totp_source) >>> if totp.changed: >>> update_user_totp(user_id, totp.to_json()) This uses the :attr:`TOTP.changed` attribute, which is set to ``True`` if :meth:`TOTP.from_source` (or other constructor) detects the source data is encrypted with an old secret, is using outdated encryption settings, or is stored in deprecated serialization format. Some refinements that may need to be made for specific situations: * For applications with a large number of users, it may be faster to accumulate ``(user_id, totp.to_json())`` pairs in a buffer, and do a bulk SQL update once every 100-1000 rows. * Depending on the dbapi layer in use, it may take care of JSON serialization for you, in which case you'll need to use ``totp.to_dict()`` instead of ``totp.to_json()``. Once all references to a deprecated secret have been replaced, it can be removed from the secrets file. .. rst-class:: float-center without-title .. seealso:: **For more details see** :ref:`Step 1 ` (above), or :ref:`totp-encryption-setup` (below) .. _totp-creation: Creating TOTP Instances ======================= Direct Creation --------------- Creating TOTP instances is straightforward: The :class:`TOTP` class can be called directly to constructor a TOTP instance from it's component configuration:: >>> from passlib.totp import TOTP >>> totp = TOTP(key='GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM', digits=9) >>> totp.generate() '29387414' You can also use a number of the alternate constructors, such as :meth:`TOTP.new` or :meth:`TOTP.from_source`:: >>> # create new instance w/ automatically generated key >>> totp = TOTP.new() >>> # or deserializing it from a string (e.g. the output of TOTP.to_json) >>> totp = TOTP.from_source('{"key":"D6RZI4ROAUQKJNAWQKYPN7W7LNV43GOT","type":"totp","v":1}') Once created, you can inspect the object for it's configuration and key:: >>> otp.base32_key 'GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM' >>> otp.alg "sha1" >>> otp.period 30 If you want a non-standard alg or period, you can specify it via the constructor. You can also create TOTP instances from an existing key (see the :class:`TOTP` constructor's ``key`` and ``format`` options for more details):: >>> otp2 = TOTP(new=True, period=60, alg="sha256") >>> otp2.alg 'sha256' >>> otp2.period 60 >>> otp3 = TOTP(key='GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM') Using a Factory --------------- Most applications will have some default configuration which they want all TOTP instances to have. This includes application secrets (for encrypting TOTP keys for storage), or setting a default issuer label (for rendering URIs). Instead of having to call the :class:`TOTP` constructor each time and provide all these options, you can use the :meth:`TOTP.using` method. This method takes in a number of the same options as the TOTP constructor, and returns a :class:`TOTP` subclass which has these options pre-programmed in as defaults:: >>> # here we create a TOTP factory with a random encryption secret and a default issuer >>> from passlib.totp import TOTP, generate_secret >>> TotpFactory = TOTP.using(issuer="myapp.example.org", secrets={"1": generate_secret()}) Since this object is a subclass of :class:`TOTP`, you can use all it's normal methods. The difference is that it will integrate the information provided by using():: >>> totp = TotpFactory.new() >>> totp.issuer 'myapp.example.org' >>> totp.to_json() '{"enckey":{"c":14,"k":"FLEQC3VO6SIT3T7GN2GIG6ONPXADG5CZ","s":"UL2J4MZG4SONHOWXLKFQ","t":"1","v":1},"type":"totp","v":1}' In typical usage, a server application will want to create a TotpFactory as part of it's initialization, and then use that class for all operations, instead of referencing :class:`TOTP` directly. .. rst-class:: float-center .. seealso:: * :ref:`totp-configuring-clients` for details about the ``issuer`` option * :ref:`totp-storing-instances` for details about storage and key encryption .. _totp-configuring-clients: Configuring Clients =================== Once a TOTP instance & key has been generated on the server, it needs to be transferred to the client TOTP program for installation. This can be done by having the user manually type the key into their TOTP client, but an easier method is to render the TOTP configuration to a URI stored in a QR Code. Rendering URIs -------------- The `KeyUriFormat `_ is a de facto standard for encoding TOTP keys & configuration information into a string. Once the URI is rendered as a QR Code, it can easily be imported into many smartphone clients (such as Authy and Google Authenticator) via the smartphone's camera. When transferring the TOTP configuration this way, you will need to provide unique identifiers for both your application, and the user's account. This allows TOTP clients to distinguish this key from the others in it's database. This can be done via the ``issuer`` and ``label`` parameters of the :meth:`TOTP.to_uri` method. The ``issuer`` string should be a globally unique label for your application (e.g. it's domain name). Since the issuer string shouldn't change across users, you can create a customized TOTP factory, and provide it with a default issuer. *(If you skip this step, the issuer will need to be provided at every* :meth:`TOTP.to_uri` *call)*:: >>> from passlib.totp import TOTP >>> TotpFactory = TOTP.using(issuer="myapp.example.org") Once this is done, rendering to a provisioning URI just requires picking a ``label`` for the URI. This label should identify the user within your application (e.g. their login or their email):: >>> # assume an existing TOTP instance has been created >>> totp = TotpFactory.new() >>> # serialize the object to a URI, along with label for user >>> uri = totp.to_uri(label="demo-user") >>> uri 'otpauth://totp/demo-user?secret=GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM&issuer=myapp.example.org' Rendering QR Codes ------------------ This URI can then be encoded as a QR Code, using various python & javascript qrcode libraries. As an example, the following uses `PyQrCode `_ to render the URI to the console as a text-based QR code:: >>> import pyqrcode >>> uri = totp.to_uri(label="demo-user") >>> print(pyqrcode.create(uri).terminal(quiet_zone=1)) ... very large ascii-art qrcode here... As a fallback to the QR Code, it's recommended to alternately / also display the key itself, so that users with camera-less TOTP clients can still enter it. The :meth:`TOTP.pretty_key` method is provided to help with this:: >>> totp.pretty_key() 'D6RZ-I4RO-AUQK-JNAW-QKYP-N7W7-LNV4-3GOT' Note that if you use a non-default ``alg``, ``digits``, or ``period`` values, these should also be displayed next to the key. Parsing URIs ------------ On the client side, passlib offers the :meth:`TOTP.from_uri` constructor creating a TOTP object from a provisioning URI. This can also be useful for testing URI encoding & output during development:: >>> # create new TOTP instance from a provisioning uri: >>> from passlib.totp import TOTP >>> totp = TOTP.from_uri('otpauth://totp/demo-user?secret=GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM&issuer=myapp.example.org') >>> otp.base32_key 'GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM' >>> otp.alg "sha1" >>> otp.period 30 >>> otp.generate().token '897453' .. _totp-storing-instances: Storing TOTP instances ====================== Once a TOTP object has been created, it inevitably needs to be stored in a database. Using :meth:`~TOTP.to_uri` to serialize it to a URI has a few disadvantages - it always includes an issuer & a label (wasting storage space), and it stores the key in an unencrypted format. JSON Serialization ------------------ To help with this passlib offers a way to serialize TOTP objects to and from a simple JSON format, which can optionally encrypt the keys for storage. To serialize a TOTP object to a string, use :meth:`TOTP.to_json`:: >>> from passlib.totp import TOTP >>> totp = TOTP.new() >>> data = totp.to_json() >>> data '{"key":"GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM","type":"totp","v":1}' This string can be stored in a database, and then deserialized as needed using the :meth:`TOTP.from_json` constructor:: >>> totp2 = TOTP.from_json(data) >>> totp2.base32_key 'GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM' There are also corresponding :meth:`TOTP.to_dict` and :meth:`TOTP.from_dict` methods for applications that want to serialize the object without converting it all the way into a JSON string. .. rst-class:: float-center .. caution:: The above procedure should only be used for development purposes, as it will NOT encrypt the keys; and the IETF **strongly recommends** encrypting the keys for storage (`RFC-6238 sec 5.1 `_). Encrypting the keys is covered below. .. _totp-encryption-setup: Application Secrets ------------------- The one thing lacking about the example above is that the resulting data contained the plaintext key. If the server were compromised, the TOTP keys could be used directly to impersonate the user. To solve this, Passlib offers a method for providing an application-wide secret that :meth:`TOTP.to_json` will use to encrypt keys. Per :ref:`Step 1 ` of the walkthrough (above), applications can use the :func:`generate_secret` helper to create new secrets. All existing secrets (the current one, and any deprecated / compromised ones) should be assigned an identifying tag, and stored in a dict or file. Ideally, these secrets should be stored in a location which the application's process does not have access to once it has been initialized. Once this data is loaded, applications can create a factory function using :meth:`TOTP.using`, and provide these secrets as part of it's arguments. This can take the form of a file path, a loaded string, or a dictionary:: >>> # load from dict >>> from passlib.totp import TOTP >>> TotpFactory = TOTP.using(secrets={"1": "'pO7SwEFcUPvIDeAJr7INBj0TjsSZJr1d2ddsFL9r5eq'"}) >>> # load from filepath >>> TotpFactory = TOTP.using(secrets_path="/path/to/secret/file") The ``secrets`` and ``secrets_path`` values can be anything accepted by the :class:`AppWallet` constructor (the internal class that's used to load & store the application secrets in memory). An instance of this object is accessible for inspection from the :attr:`!TOTP.wallet` attribute of each factory:: >>> TotpFactory.wallet Encrypting Keys --------------- Once you have a TOTP factory configured with one or more application secrets, any objects you create through the factory will automatically have access to the application secrets, and will use them to encrypt the key when serializing to json. Assuming ``TotpFactory`` is set up from the previous step, contrast the output of this with the plain JSON serialization example above:: >>> totp = TotpFactory.new() >>> data = totp.to_json() >>> data '{"enckey":{"c":14,"k":"FLEQC3VO6SIT3T7GN2GIG6ONPXADG5CZ","s":"UL2J4MZG4SONHOWXLKFQ","t":"1","v":1},"type":"totp","v":1}' This data can be stored in the database like normal, but will require access to the application secret in order to decrypt:: >>> data = '{"enckey":{"c":14,"k":"FLEQC3VO6SIT3T7GN2GIG6ONPXADG5CZ","s":"UL2J4MZG4SONHOWXLKFQ","t":"1","v":1},"type":"totp","v":1}' >>> totp = TotpFactory.from_source(data) >>> totp.base32_key 'FLEQC3VO6SIT3T7GN2GIG6ONPXADG5CZ' Whereas trying to decode without a secret configured will result in:: >>> totp = TOTP.from_source(data) ... TypeError: no application secrets present, can't decrypt TOTP key Note that when loading TOTP objects this way, you can check the :attr:`TOTP.changed` attr to see if the object needs to be re-serialized (e.g. deprecated secret, too few encryption rounds, deprecated serialization format). Generating Tokens (Client-Side Only) ==================================== Finally, the whole point of TOTP: generating and verifying tokens. The TOTP protocol generates a new time & key -dependant token every seconds (usually 30). Generating a totp token is done with the :meth:`TOTP.generate` method, which returns a :class:`TotpToken` instance. This object looks and acts like a tuple of ``(token, expire_time)``, but offers some additional informational attributes:: >>> from passlib import totp >>> otp = TOTP(key='GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM') >>> # generate a TOTP token for the current timestamp >>> # (your output will vary based on system time) >>> otp.generate() >>> # to get just the token, not the TotpToken instance... >>> otp.generate().token '359275' >>> # you can generate a token for a specific time as well... >>> otp.generate(time=1475338840).token '359275' .. rst-class:: float-right .. seealso:: For more details, see the :meth:`TOTP.generate` method. .. _totp-verifying: Verifying Tokens ================ In order for successful authentication, the user must generate the token on the client, and provide it to your server before the :attr:`TOTP.period` ends. Since this there will always be a little transmission delay (and sometimes client clock drift) TOTP verification usually uses a small verification window, allowing a user to enter a token a few seconds after the period has ended. This window is usually kept as small as possible, and in passlib defaults to 30 seconds. Match & Verify -------------- To verify a token a user has provided, you can use the :meth:`TOTP.match` method. If unsuccessful, a :exc:`passlib.exc.TokenError` subclass will be raised. If successful, this will return a :class:`TotpMatch` instance, with details about the match. This object acts like a tuple of ``(counter, timestamp)``, but offers some additional informational attributes:: >>> # NOTE: all of the following was done at a fixed time, to make these >>> # examples repeatable. in real-world use, you would omit the 'time' parameter >>> # from all these calls. >>> # assuming TOTP key & config was deserialized from database store >>> from passlib import totp >>> otp = TOTP(key='GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM') >>> # user provides malformed token: >>> otp.match('359', time=1475338840) ... MalformedTokenError: Token must have exactly 6 digits >>> # user provides token that isn't valid w/in time window: >>> otp.match('123456', time=1475338840) ... InvalidTokenError: Token did not match >>> # user provides correct token >>> otp.match('359275', time=1475338840) As a further optimization, the :meth:`TOTP.verify` method allows deserializing and matching a token in a single step. Not only does this save a little code, it has a signature much more similar to that of Passlib's :meth:`passlib.ifc.PasswordHash.verify`. Typically applications will provide the TOTP key in whatever format it's stored by the server. This will usually be a JSON string (as output by :meth:`TOTP.to_json`), but can be any format accepted by :meth:`TOTP.from_source`. As an example:: >>> # application loads json-serialized TOTP key >>> from passlib.totp import TOTP >>> totp_source = '{"v": 1, "type": "totp", "key": "otxl2f5cctbprpzx"}' >>> # parse & match the token in a single call >>> match = TOTP.verify('123456', totp_source) .. rst-class:: float-right .. seealso:: For more details, see the :meth:`TOTP.match` and :meth:`TOTP.verify` methods. .. _totp-reuse-warning: Preventing Token Reuse ---------------------- Even if an attacker is able to observe a user entering a TOTP token, it will do them no good once ``period + window`` seconds have passed (typically 60). This is because the current time will now have advanced far enough that :meth:`!TOTP.match` will *never* match against the stolen token. However, this leaves a small window in which the attacker can observe and replay a token, successfully impersonating the user. To prevent this, applications are strongly encouraged to record the latest :attr:`TotpMatch.counter` value that's returned by the :meth:`!TOTP.match` method. This value should be stored per-user in a temporary cache for at least ``period + window`` seconds. (This is typically 60 seconds, but for an exact value, applications may check the :attr:`TotpMatch.cache_seconds` value returned by the :meth:`!TOTP.match` method). Any subsequent calls to verify should check this cache, and pass in that value to :meth:`!TOTP.match`'s "last_counter" parameter (or ``None`` if no value found). Doing so will ensure that tokens can only be used once, preventing replay attacks. As an example:: >>> # NOTE: all of the following was done at a fixed time, to make these >>> # examples repeatable. in real-world use, you would omit the 'time' parameter >>> # from all these calls. >>> # assuming TOTP key & config was deserialized from database store >>> from passlib.totp import TOTP >>> otp = TOTP(key='GVDOQ7NP6XPJWE4CWCLFFSXZH6DTAZWM') >>> # retrieve per-user counter from cache >>> last_counter = ...consult application cache... >>> # if user provides valid value, a TotpMatch object will be returned. >>> # (if they provide an invalid value, a TokenError will be raised). >>> match = otp.match('359275', last_counter=last_counter, time=1475338830) >>> match.counter 49177961 >>> match.cache_seconds 60 >>> # application should now cache the new 'match.counter' value >>> # for at least 'match.cache_seconds'. >>> # now that last_counter has been properly updated: say that >>> # 10 seconds later attacker attempts to re-use token user just entered: >>> last_counter = 49177961 >>> match = otp.match('359275', last_counter=last_counter, time=1475338840) ... UsedTokenError: Token has already been used, please wait for another. .. rst-class:: float-right .. seealso:: For more details, see the :meth:`TOTP.match` method; for more examples, see Step 6 above. .. _totp-rate-limiting: Why Rate-Limiting is Critical ----------------------------- The :meth:`TOTP.match` method offers a ``window`` parameter, expanding the search range to account for the client getting slightly out of sync. While it's tempting to be user-friendly, and make this window as large as possible, there is a security downside: Since any token within the window will be treated as valid, the larger you make the window, the more likely it is that an attacker will be able to guess the correct token by random luck. Because of this, **it's critical for applications implementing OTP to rate-limit the number of attempts on an account**, since an unlimited number of attempts guarantees an attacker will be able to guess any given token. **The Gory Details** For TOTP, the formula is ``odds = guesses * (1 + 2 * window / period) / 10**digits``; where ``window`` in this case is the :meth:`TOTP.match` window (measured in seconds), and ``period`` is the number of seconds before the token is rotated. This formula can be inverted to give the maximum window we want to allow for a given configuration, rate limit, and desired odds: ``max_window = floor((odds * 10**digits / guesses - 1) * period / 2)``. For example (assuming TOTP with 7 digits and 30 second period), if you want an attacker's odds to be no better than 1 in 10000, and plan to lock an account after 4 failed attempts -- the maximum window you should use would be ``floor((1/10000 * 10**6 / 4 - 1) * 30 / 2)`` or 360 seconds. .. xxx: The above formulas are not accurate for 10 digit tokens, since the 10th digit takes on fewer values -- subtitute ``3e9`` instead of ``10**digits`` in this case. passlib-1.7.1/docs/narr/overview.rst0000644000175000017500000000734413016611237020605 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.crypt`, as stored in mysql and postgres, and various other places. Passlib's contents can be roughly grouped into four categories: password hashes, password contexts, two-factor authentication, and other utility functions. Password Hashes =============== All of the hashes supported by Passlib are implemented as "hasher" classes which can be imported from the :mod:`passlib.hash` module. In turn, all of the hashers have a uniform interface, which is documented in the :ref:`hash-tutorial`. *A word of warning:* Some the hashes in this library are marked as "insecure", and are provided for historical purposes only. Still others are specialized in ways that are not generally useful. If you are creating a new application and need to choose a password hash, please read the :doc:`quickstart` first. .. rst-class:: float-center .. seealso:: - :ref:`hash-tutorial` -- walkthrough of using a hasher class. - :doc:`quickstart` -- if you need to choose a hash. - :mod:`passlib.ifc` -- PasswordHash API reference - :mod:`passlib.hash` -- list of all hashes in Passlib. 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. * Perform rolling upgrades of existing hashes to comply with these changes. * Eventually, these policies must be hardcoded in the source, or time must be spent implementing a configuration language to encode 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 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. .. rst-class:: float-center .. seealso:: * :ref:`context-tutorial` -- walkthrough of the CryptContext class * :mod:`passlib.context` -- API reference Two-Factor Authentication ========================= While not strictly connected to password hashing, modern applications frequently need to perform the related task of two-factor authentication. One of the most common protocols for doing this is TOTP (:rfc:`6238`). To help get TOTP in place, the :mod:`passlib.totp` module provides a set of helper functions for securely configuring, persisting, and verifying TOTP tokens. .. rst-class:: float-center .. seealso:: * :ref:`TOTP tutorial ` -- walkthrough of setting up TOTP integration * :mod:`passlib.totp` -- API reference 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. passlib-1.7.1/docs/password_hash_api.rst0000644000175000017500000000020313015205366021457 0ustar biscuitbiscuit00000000000000:orphan: .. redirect stub .. seealso:: This page has been moved to :doc:`narr/hash-tutorial` and :doc:`lib/passlib.ifc` passlib-1.7.1/docs/_static/0000755000175000017500000000000013043774617016675 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/_static/masthead.svg0000644000175000017500000004012513015205366021173 0ustar biscuitbiscuit00000000000000 image/svg+xml 0111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011011101000110111101101111001000000110110101100001011011100111100100100000011100110110010101100011011100100110010101110100011100110111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011011101000110111101101111001000000110110101100001011011100111100100100000011100110110010101100011011100100110010101110100011100110111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011 PassLib PassLib passlib-1.7.1/docs/_static/logo.svg0000644000175000017500000002522312214647122020347 0ustar biscuitbiscuit00000000000000 image/svg+xml passlib-1.7.1/docs/_static/masthead.png0000644000175000017500000002172513015205366021165 0ustar biscuitbiscuit00000000000000‰PNG  IHDR´4#}êsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î< IDATxœíyœU™÷¿u—î¾½¦»³‘TV‚‰Œ ‚ãøª€\DaD Ä× 7f\`À Ä©WAPP6¯Šã>*›‚ k€ÈRY:ÝIïÝ·ïRõþñ<§ëtõí$D'ÏçSŸ[§êÔYŸóœçùçœë¸y½´—þ^(õ×.À^ÚK»“2æ&(ø)7ï…N¡›÷¢ à›¸U ;.»yÏ|›vó^ÙúÖqó^EãfŠ›÷Bk¾4nÚÍ{%ýÖÄ/%Ó¶ò ݼWµÂU«ÜYÞ^¹¬¼RnÞ«&ÛÀ¼3m`½¬6À¼Ó÷ìñþº”ñŽNNPð+ú¼ 4hÊ@Ñß1 Ñß<¯ªÊ•‹Î§óð8¦9ççÊ'Ñõ­ÜuólÓ<M¿¤ejŠú;¬¿£švQã×k¾†¡ ¤qÊ@.(øÃZ§z}†Æ«*:Hô»:ÀÔÛ0sH<ƒUƒ‚ŸÑg+½´~7ÎìÊÔFìeð˜L‡eŽ©ê}F¥– §-)æ R® ¤‚‚Ÿ>õÿùÁˆÎ"^Z±‚Æ{nH]ñO¯H­Þg~˵óf^Ù1³í;zGjíÝßM_wÇ ~£‘–‰´+@Z™¨ªáŒJà ˜f61ƒ-çx=ì´5~ÅŽ«iWõ>­ß™o# ²â§4¿((ø)}ïh¹ÑrG¦M•‘÷ªt/0e¬ûqç™N2ìXq"À¹ùdÿÑ)Î¥ÑѤÒE§ø‡oóó¡èò>Èý@õŠóSW7·æòû½tß\]CcNÇ Õ*<ûøÓ'÷vuyË©þkŽ,0`Oåšç8ƒXepìr&Â6³–ÏH„Í´Ÿ™úÕ¢”õ[µ¿SÕ(Y† ÷ ¦ßK{ˆÆuRDmpi>ƒLÍŽ›÷†‚‚ß Ï"7ï _{œvG«såÂ}r³çåœt&ËØ˜Ã¶-¥híª¾Ñ¾.Zº8{ãŠWÜ’Jgéß6Ì3¯ÂÎ}æe/?°aݪՕϭ¾÷¨3+oFݼ7ü-GÅÍ{#AÁoC˜l aÒ6dèG«Më2€0¨)÷@Pðë ¨õÈi=Gݼ7üfM»„HþF 5ÍœþŽ"Œœpó^Qg:Ä(«î±~oÔ¸ª¶¯Õ‘ÛÕ·ëfDíU}¼ÅJkuó^·†Ûˆí†hEtâ!mƒf`ÄÍ{cšv#Я8uPïæ½~M«™ñ*ªS7h—¹El,ü<²Âã6°!Þý¼Ç)(øG÷ õ¸x?pM"Ú9Àá{²b*#¸®QA𣋠àgܼ7„â­aÝß¿­RŽH3×miJeœ/ÜöUμ÷z.H¥Ø¯±¹‰È1(˜^Nš¹ gŽ|Û2‡¿þÄô²C¯sœ,¥â =›§³›PÌÍ{Ûìºyo ±1V¶Zá1`ØàjuóÞVOO»yo‘ÀÍ{}æ^6€> Yá»ÁD›ÜÛäU¶ÂE&""7ïY³@£ßש¤*3&¬ÅŸª–b4Ć_ìôf&ª°ï¬çJà[À£{ªIÚÆs#Djeí¸##\¶i]±RhmodÙÁsZ—/oý¶Ymç­xÕþ¹L}™95~‘"•Îbøï™‡ï ] ‰ÅSAmæ~ª°$d­÷µêœE$¹ýÝÎä=éÁÏ­ôÍ £_² f0l†4 ®ùÔHêEC[á_%#¸yï7ï½×Í{/Å4»“lÚnȪN¹9 ÇzÏY?ñÖ]õÚo_òÈ}[>¹ü°}rsZÎ9cŽ0h•¹$36¤¨”û(‡Ã¦¦Ô±Q®¾ëºh åpÉ©·p9“™ ü÷ß4’kºҹXüx*(ø_SÉ=^l ~‘‰»#žô^ñó”5 Í*k•‰RÜ|k`¼4*¹íex5”@Q( ‰ý¼`¾ àoöÙA´enÞ[=ͤ¯f!:ò_€ÿÜ…â=or¦-㹃VxÌÂkJÐüì÷/á´Eóo´u4E³ÛZ'CKG+ ÍæÆÉ3z«Eª•ªå­T+[©–{ª²i CÃÃÜ÷©«xÿ½Òçæ½ž àÏA:þsÀ{v±žEàzà‹ˆjUV<‡,÷k[´# 3‚¨-V›¤µ "ón@¤|äæ½AÕïë;$T!Æ´mºj}këÔÆe‚‰5©¿KüY@“j’NvóÞ»œÑΕ¥B<3^ìæ½ÝbN‡¶é¦“üRœ˜®KŸ@Ä솦Üá/}åŠÖí1s…yt¨‡°Z$…0‚Íkä¦Sèe祺x%ð)`_}é}X,CŒ8›~o–ñÿ7’Ƈ'°^ý1âîuó^ð”ãÅÇÐ6Ýt’ߒʦºxý«ë'1³ÅÐÕÊ6ªe‘Λ×m¬tÅQÒif¸ûÓDkW2Z 9òô[½GjåüŸ'h°âæ½I8¦Jö ‘N³U˜/¹yï‚ÝRé¿a þ+€?Y>ãæ½/¾€ùÝG‘)­¨áVdú4n›mÀ µ¬Û ô锘=º@æÞ­£ÃcórÍõˆ£&¬V¨Vî¥;(Ž<ð(‡ÿ%Ïv¯ç¬Ù iji'Ó·…7¨'\3° aÌL4â ~›÷JªwΆݼ×ü )þ#VôÓ ô»,sóÞ€†=vDÕ‘Å|¹؈¨Emƒ:ÁÒoSˆZ1¢zoƒ¶gIÛφí:Eˆj³ÞÍ{Ö#–í®[qcZÛµ×Vf#:q?°Éôß_ƒ´œ3Õ]ÐÃí´:‘z ¸yoÝÎ~gpè&Dÿ˪ñÑ‚è~û½&Ü ¸ÀZýÝ, þ:`!Ð56æ<:<04/×ܘ9E–«T«„ÕAJÅ!‡Íç‰.`aTåwaÈ™Q™z²#%ŽÒŽ]Œ0Ô|ýM!Ìa“aøyˆ~» (øÏiÜk“%w¾bݽÀ\`[PðgêûS€#ƒ‚(Ò16•€øŸ àJóiq¬Bñp -(øýˆ®Ý¨ßÕï>ü}I¨BAÁn þ÷(±¬ß'«4âÚÚŒ`¼çhÛÔ%ÒÚü¸¸Ãàä{Š‚‚6²(ÒŽ93Øî$žE§“ÞYÀ‡€Ö³~àà nÞ›„žØd¦â&¤3æhPÉ3¤ï Æjü‚Ѱ1”†Ý¼7´¹»òà@ßàØ8ªá¤Ã¡Z`°w3½]› «Ô5”I¥™×wøÈåç˜tš/74ÑEP.RêàYÍ{H%h£…'É4¢)÷°ÆFâÞDüƒµÜ#§1:?Ëdfaœ¥ÀÙÀjý5Ópeò4¸5AÁ? X\ Âd½„^ | Ø€Hþм´$ó ˆ@¹ x “6ÈÊÛqÀw€‡j¼ßÝôZd`™Eª]¢ à7ÿÇÀ·±˜Y© iŸŸÿ"k a•Ãö 6€¿±¼G¬øÆ{Ì.¸qŒgî¬Ìš[Zë" ‡®”«lYÿX´yí3ÕRy(ÊÌ[Ílš3M=¹à•‡ΘESS;©j†û)ýåI~H©é¬óHËvÂw€'ñŒw]Fgˆ‰pØåÀ-È€ÉÿZà\d5WÏ †fHíÎÜ( ³K;þYÚ„0÷àåÀ¥Ä8g2pí4nÒ¼×ÿ܇ÌZ}ÈuµœŸÑošk5Òn¦~ ‰¨s­»Fp+ðzMëjà1¤ÝDl¡fâUÈuÈÍ$r4b Á¤¡sȨH¹yo½ê39D m þ<¤áÇtIzñ×?Ϋ^yXõ‡ýãëšp²àdV­,nXý„34šºçž‡*7|þnºþßyÅR—;g/ ¾­“t ºEP.ÖµŒöôpÇñçòQ7ïm þ~ÃlCôÄe€ãMU¤³ŸEÔ¦ˆ±û\PðÛuè}z:ØÍ{~?܈`¨U„{fºê”å—Äç“ÀÑnÞÛªè$8¬6ÈÕH'ü?àã£FÄ0žq×ý½vcöh{—uVü5pŒ¾ÿ"Í+$\QUmDÔ©A7ïÕd°=aÿOÀ+4x§›÷¦T9F!ȺÀ{ݼw}¸+Á1K­ösuÛžMfYdª3dÔ ¬E Þ˜¯9âÐëï\|àAM8pÒŒ ³ñÙ•åR5|‰÷Ówo–xðé?ò»çù+z6p[Ï–6·‘rR¤*%J#ƒd‡ëŽëæ\3ͺyoU¢ÌO«Ne(róÞãz?ŠH+S®^ W}6 =jâ»yo½ÕhFBÿÉÍ{]z¿Þú®Üü«ˆ%àÈÚj/«kÚÊ4†6'–õ‡¬ûrPðo%fè´QY”RLôèVõÆÐøì¢Lý#à_ˆý¹_ì´Á¾ï«õÒÍ{ þ¥À—õÑBàŸ‘Yjí–-B7Ÿâ¿µ®¡aAÇ\£j¬Yù—Âè¢3o÷6'ã¿óvoÕ©?ô)WXÑßË'ûzøôÈçã°èÔ[¼÷/x0ÖwÙÌpùâþ÷Þß’P+’JÕ}­Ggûc º]×2y5p!±DÑß·G·kÏZ«—/fú¯©˜ÙŽƒºH(V+Ò´ÝG“ôÝû™Ô†ôå‹<¤Ù C}½ õvôõ‡WlïÛwÞî­¾þ|Ë$5f"Óß?3Ö/c©}™ÖïÜAòဟ&#)<÷,¢",ø|¸¸V¢nÞ[üU’´†-8:(øïpóÞ÷§(㯀6uÎú» W<1Ÿ ViÖŠg`»¢sõê’p#⦹y/Ð%Ó¹ÈtMPðç"ÓÙ`ý)©SÛfÌhíœ3Žj<ûØ#O¬®~Ùû,ø­$¶IõíY~\uÍ9š×°›÷Öÿ%ˆ×ÜZ¶ƒ´B⢹§ÒLPð·f ì˜4Ò ˆî¹ xZÓžÌR[kjkÌBÔ‰¡@\Q;ÃÑ Ǹ¢¦œzDuêôÄÐEAÁ9ðAD5ŠÕ#«—Y0\UU-ເ½ ö½ àƒ¬šè3£¦M…ý­Ób†^T+‚ìG#ÄtR'‚¹†ºhÑM|l@“›÷ž;`)û8)ç‹‹:¤ÉxÕõuobdppëßà*`®Bnˆ]>»5/3ÎvóÞ~‹›÷žF;Fã?n};† ÉitbÀÍf" ¤'¸yï$7ï=I µe† €NeœM¦ ܼ7‚†¦‘)Ïìí“ܬõMN˜ˆûšeäŒe¹'¢í¹L†ÀnÞ{ðiàÈñ]7ï=Fmi 2ã4©cÃ?Kƒ‚YPðD¤ûï"ÎéÿŠ €"2i•Ìf‡zÃnÞ{;p*Q)ó-àWŠˆ$Ï?1ímÜK·!6Á¹ˆšdÓ~ÀO‚‚ÿc]ü{§r­‡)]¡1[°TŸÞŒ`­Ý®¸3ÎÐèwëçÎHq–» ÕÐÔ ¤‰¢”SqÂý~õ­ÔçÞy<ó€Íš^Ьyµ!ÆÍ|Í÷)M×èµ ‘“’kxP þ\e˜¤1àhÚˆñ4بðU'28jZs4àgÈ’ù“È©Zñ‹ˆÊp‰›÷>5Íüæ#¾Ùg2yê¼ÊÍ{œFZË+©nDÀGݼ÷µ)¾{1-¬ì”·]PðoCürîqóÞQÉ8»Û9©ì¥s/Íf±½èÂêNj˜9 *õí³ +࣠pþOñ/̹æÍ?õjN5 >-Ÿ kÝéÝîÄÉ73óÙnÞûötòŸ.¹yoð® à_ƒ !K­×çÿ67ïýv'“{‘Ê'!êËL}î—ªD-¯·ím5Û.ílÿL§“qÝÚðØ;mž¨•Ží>ÚŒÀD£ž8ï÷Ç ÿžo³Ìq2'Ïß÷€¬ñ¢Û¬ª öv…³ç×Õ9Î Q’©ƒ}–+ŽÛø _ÞRap£êæíˆ:SÑ|ÛåZS¦Yˆ7Ÿ9Ñ©èFT€™L6‰ †N²*é·Mú-šNY=4ÆÛQˆ^jèWZÿŒ~;âÆ§—¶ «‘“H•qxŠTÕ ]Ù1nàIJŸ|Z‡HÈÀmÀë¬äÎ þ]ÈìcNhÅO\5F¨Yöv€kZ?Òt²òI&öIÌ6MA²3ÛÓ¦Ú#Z+­ ǰ‰-vÊà6C×t¼JéÇͨDPð+®ó´’.Ò‘ Suu—Ï[¶¼>iR”Š[Y»ò©J×ú¾;V>°y¬kýX¥Zߌ(Çj•Ò•ßì¥ƪ״ÓÄ~$°?¢.C˜{?-Ë-£Ùêd7Æ ¤ók£ì«ázÊËX×LbG,ñâ24Œ(ãš“KsìéëDôÕYÔ¦ÄΘÄOçÿjÍÃ0õ¸ß5ꩈ ÝVZf©ÝF ¾¯íbÇR‰k ð6&êÕ‡j\ƒug¬úÛd˜Ê™âºø3‚…§¬øñÆÑÉé™|35.›’é$ËbÖæiü‹Y&!Ðó&E» qÃ$öùM¹yoäKã ÇqŽž»xÿ4¤ +lxfåØð(·œñ£wŸöÝoÜ´žÿ^ý(ÅÞ.Â0„­ê沟Ý-@¿ºkÚ˜ö"MMÇUÁ“E0åA߬qŠLñuó5#†T‘úö™Y&žQ×¢évZé4è÷­CYZ¯éšgãdáÎæ¨Gã} ‘gè» ç_ë•Õ6°ý|G™(}«À—%ïS¬w6¤µ^kßj;-›™Ío-œêêD|'^†£É]ÐæJJhüYë2}a“é;­Ø÷X~±¾û%² z’ä7#¥b%RA 4Ã`et1∃³/\þÒÆT*KŽR­P­Ž•¢×Ýüÿ”£ÎâÁoÞÊÿù Yñ…­]IÄèÅ×p-Së·µ¦#3ýTµL˜ÎH~cË8ÿ×#’ÏnHˆOÍ3©½"ƒW î,ûTÑym?dS–d8Äêɉ‚aw¼élƒ=ÛÎH°â™¼ ®ðÕïí|m¼ö^­¯i“_-ìÝnW»¿Î&^S0ªŸc}“T7Ìó “G-2}bԨк7×5nÁðäžPFEù 2 dÚª iJuÀ¡Ï柞Ë5î;kÞR'ŠBÂÊ auùK››[ÚFš7®»î®ëØX)sÞkÎaðî[/gQXfàÞGÅ5(øCZøvm 5ˆ4Ì!K™ë‘Ž]ˆLÿÆ×z‹e4{Úwø>ýÆ%–jÈ cv»´#R|6ÙÕÏ%÷HþQ#Ì‘'ÿŽHñ!Œ¢"í‹Ìn3‰}¸[ˆ™t.ðd'‰9ƒÚì(Ör¾Ú ߈̚æ|‘41CŽº¯¨¦¬ß…ˆ$q{½“x`É^,:Ù´ÙÜ“…Ïû­ûn&«)D5´7FÌBÔ£5옩g"ýbﱺÙíÿn{²{Å®Óx™Ça» ñw AÁ?Ëxý|ç¾³O/;ôˆeísQ­ôêF×ë*}„ öB÷†ªVŽ–ùÐ?ÍŽ<¨HTÔ4’-}ŒT;‘rµ“„IW§!ŒeW6yo?» ÙÒdÓS³· FÖ D58YÔ¸?â/ò!ëÙOˆ5B|¶×"/ƒ¨E‹‘¢‚œ „ÃîüÇ­xU·ß¨iåÁ°ŒåèCN3Ziåߎx .g×ÓÞŒ¨G¦¯þ Ñ«kíòi«+‰ÏT1}½Ž‰³D¨Ï¶ êoˆ ’EV¼oej)W*‰GÛ°É]×ñöæÖ¦¹ísV‡+ýl^³f,)Ö5· 9Ælj…\ Í[yywÀ]W~ˆö~&Žø˜bÞÙÓš¡í13Hg"’q*f¶ }‘ï!68÷×ËÄÿ±ÆÛ¬á'™è:ÚH£Î€Àh!²!HòZ«xDê_0n-•æ+ÈñZÿ i-×+IEàûÈL³ž‰m؇  çãe¹•‰'ËÔÌ Ògf—“-¡¯@vê¬@„FJ˶¸F]ˆšq›æZ—ÍÜáVî8ÁÏ–š2ë—¿üØ9Í3Ú©V¶Òß³ŽÕ<»-" ²Y–Ì^@K®E‘úº {6ñ?¯>‹7153%·=ÁD=Ô6 Œ;èÔž¶ìQº aŠI£·Fõ’Sá|d?ÞDMè!Þf•ôë~2ýw[×£ˆTO–o–Ƈ@OsÑßzýq’²Ë•,§©çÄx½f#Œº^Ó¸›ÉËâæÛAPÌÁî¶” qí|í6ú3±ŸÒ2ìKí~1ß®$^ÙKÆ1êÊ2DÍ«Wˆ0ò=ˆúg üpŠ+–Њ‰¶£›]ƒøä{J§¤Nojílié˜CµÜCXdýªõ#Ϭ¾üÏŸá;w\Î1cc|¥!ÇŒY hÊdaÛfÆnù_ц3ÌØ¯W³†Dš´h¼1„gkØø=ÌÓJoFpÛÙZ¹­ˆ>Û‰è׈dkA$F72¥· Ó<·Ñ?Dª¦`|)#R`Pãµ!:í¢‹7iÇTÝô7ZŽ™öÓZ†²ÖÁ ½Ú)-»±ê#+\GìÉgŒ)ÓQöÂÑb¯³íÍrIÏÌ|0‘1lÆ®•frÐ'ß-L„§2üÌ»dú„áW&ÞÕºßá™q,ûÔ‚0ÝÖ7K)ç Kú‡Æ°2@µ:Hµ:@& K]þï5òö3.ä7Ç¿—×ùÔúUôOSå×_ÿ" SAÄÀ5Eb(©aš-ĉÑEŒ£š4>¦ï 2„HQch5#z¥Ùk׃0«y¿…µ1†cÂ|e„YM‡”f60J¬Ê_’v´ mäÈèÊ-‡a"ÃèöµØiEL–H¦J5.j„ ÕbÎô.¦³¿Ÿ*ß©Ê9•dM>«Z¿ö&î'µ­KÙwÎ9‘£'ÕP. ‘­sÆ—·î5õÓ˜­çâ;¯ä¼?=ÎçOþ07-šÇÝŸýŽ-ÜÍo53s©­FDÄ0M…Š3’È.°)hE/“N™˜iÍrº‰SEMšø9{:µÿó¤dåcò5i›|âVJä]±â˜ç†ñL܌ޛº›øæÄv²SÍaIɶ=)˜|o«+Éߩ԰©lsOߥZêbò~*Þ¾jètëþ²5kੇ2€Óºÿaå§ÊáoïfËqdž«†¶GöoíNårÕlÊ ÇÈÔÁŒ™dës´w6ò†3ßÄqõ¹àr~ñäêøHYâ­C¦â"CâeëFâ]MÄ»£#}7@̘Dú™#F‚fÉ׬i™ô‡iHÐccÆ)½7†LEËg˜¼H̼&®Áñ LgÎ,1H‹=pÌ}5¶ãÚRɨ»rU¦¸·%áŽ$¿)k­WK’îî«–d®U^ôˆ*„‰â“7›ME^²€ŽÏŸÇ9 ç:in‹²³\r™,D‘ABï¢Þ.zŽ>kü—4ñ”k¶Ò›Å aàœÞ#}ÖI¼ð0Œ¨1h9Í)©ÂðeÍ«H—ÍœE–eLqróçñŸ5çOØ»`ŒÄ#Þt;¾uJÓ±‘’’Æ1Œk\:Óš—çÐ6©•@þ¼~ÔÁaÔ%S6û‹nÞÛ+÷†®ZÏBDŠØáªN•6^k` aU‰™Þ„Çñìo#â)8„q})²òMâÇÀ$ÝF ’õ°íøÉçv^Nâ]­ø¬neÌíZâ®±kÇßK{€ŒÞgÜ VlôGÇ•?j7*IäÆBi:¾Lì+k0Sã+` ,³ lVÞŒk¤¿iiNÎ7’z,ÃØºbÒN¯¼h-ùóæDÍÖ§¢êÐuš—9ªÁ,˜… {±Æ@m¶zcŽù2RÝèÔã[ˆ¬ÙfïŸÓ¿ÀôÿÙ‰ d©IEND®B`‚passlib-1.7.1/docs/_static/bb-logo.png0000644000175000017500000004141613015205366020717 0ustar biscuitbiscuit00000000000000‰PNG  IHDR€€Ã>aËsBIT|dˆ pHYs$é$éP$çøtEXtSoftwarewww.inkscape.org›î< IDATxœí½y°eG}çùÉÌsÎ]Þ}û{µ—jÑ^Z!ˆU`³ôxÜnšp»i÷ÄŒ;ÚÜ11㉰=KODǸÃán·—¶Ç@c›öƒ‹@HÆB€Ð^Û«·¿w÷s2sþÈ<Û]^½‚*©äè •î»÷ž{–ü-ù[¾¿_Šwÿþ·,/ºa‘ým­E @ˆ‘G „@HµÃùÀh €TÁˆC,Æh„”!ýG¬Ýἃ÷zùOzùkLþF ßã„uÿY‹5î7RÙñÖ¬'|:L ”ʈ)±Zc…ÉŽEüô:)ݳ;¥w—ÅxQ2€"¥«û{„ä»ÏGI¦-/„5Z‚ÅÀy…%°^c¸ïÇ\;{Súf̽pãEÉ€“ÈïëûÁOÅ¡ÏÝó!ç{Ç‹—žÏa¿3éEÂÿFžªÿQ£¤Šç¸Œ™á¿1@qŒ |‰àçQ[Za2†HÏ{2Â?ØIMŸoâ~;šèÖºƒðÆiÊVäÇ ÄeÉ/N¸Ð5y—Çg„/=#¸µÅ/JŒ 2—2õûÜ«Óef¸ÜáÅÅ;Ijù‹=®A+~,á=ÑÝßÅãw SXë\T§wŽÒÒð3ÁeÇ.º6àß&<%‚5kl!27À 6;¡;§(DðĈc­ÅZíi"Ü•Œu×+1BPŽÚ±­pç ¤Ì4‚e&ðO´c4óR牬§¡›@!äpäÎZLÒOÇb2Ȳ9á-ºßÁíßd¢ÂJv-€¤ßÁ$0X@‚J­°N»¡ã:îù0°EEXÀb‘6æjùiöªGì6¡Ù¦&›D²ÃéÞU<É›9¥oÂâA‰¬A÷{ …VIFȰâÛ:&ÀZu´€tÌ)¤ E[Ï€ŽAàb”.=ØrÔÌZ1zà!ñ…È ÀÄýì³tr° ûmŒ1Q !\Ø6îv°Ö ‚JÎ(ÚøóUˆ»m¬Ô`¿|­ÖAT!éu°6AÙ'äGyYåƒÔÅ*µé½DaDE¨ ŠTuŽ,?À­Û§Ç ö^ËãñݜԷ"„ é¶A…Da ¤Äꄤß%T€¬@'}°– ¬‚pF“ÄN“”´KÑö¸¸Jâ3€Íõ~)±I‚5:{H«]R%%¾û;Dë>:‰½t[ŒNÐqL¹÷ÖB‚€¤ÛAÔUÆp2óx¿Ö©”§kµ¿%FÙúnM›[ªÊ]µ‰û\Ãão$ªOù©RîU(Ž¢è¶·Y;ýÓ'äæ­ò½ø.>±ùoЉ$ +“ ¬BH…Mb’~— Z㯗hd¥æ§1ŒÖ¨dù¿D^,.¸¤ [Êś͓0«NãÕ9"·-n©0I‚ £l‰°Æ­çÖh¬µLqŠZp’€5&ÐTT0<«_Á†> t#”Ì— QÿºµÝðºúäÖÚÇ8tÕ ì?va4X¤¿ïté’€¤ZŸãÀÕ¯æÀÕw³¾ô-ÄÁ{ä¿æÏým{ Fk„°+ºûØJ p (c5Êç+RãP‘=Ûȵµîþpò—Zì ¯Jþ´åñÝ“db´Æú÷{Å·¸#úÏQ÷Î**tgпʺ=Ê#½7ð-q ›ÉUYZØbå¨Ý+£ßæÖÊǸöÖW1¿ÿnjüšî žÿK™Á½·V39»Ÿîxã3¼W¾¿ØúEžKn÷Ë—rYG£1:AJå˜Þoˈ_ °fd2Ë1ÆÅK1_|(}§û4yJÕåóÓŸä.Y*™NCLsP}“WW?ÄAùwÔ&+9>KcºJ†¨ †€íõ.+KÛÌŸù0wVÿ3çôUüeóÿ`“C &€öÈGxYôŽßp+óûR"rñßf0ºƒ1]‚JÀÕ·¼œçý?&~?ZÿwœÒ7#¬3VN°Ú­ñŽ‘ Ö˜Ì&ȼcKs2zŽwKÇÅeïÒ‰9ŠYGùïS|ãþgÝa,Ãí•?ä5õßfb&âÈñ)fj¤k²[ŸQ„LÍM257ËñŠí6ß}øYÞ£~šmý"O'/Ã1»Ü›¿ÂÔü>ö½–"q­(ÙXÎ>ý ˧ž ÛÙFÊ€=¯cñðMDÕc@Ì«÷ÛÜ£ß[ùM§½¼ô[c=áÛ™º—™wà]ÍAOµ8ÇÓ T7½ãúå‹u²”üydÌYØBÊòqIˆÌp ‹‚ŒøÖtÜG'}Tr{ðûÜUýmŽ]Såê&¨Õ#ÑUV¤Œà_…¤R­³÷Ð>úíGã뀓½ëÁ¦ÅI^]ÿ=®zÉíT'¦ ¿U^Å+º­m¾ñ…?båÌ<¸|3¯ÜÀÉÍYdû6žý"•ZJ­‚Ñ-Œn¢“&•ZŸÞÚ9ºÉ§ãk1I‚Õ ªRñ®¨³dà]ÃtÞ¬Á˜CódÚpÔwßϸè`Ðp){þÇ7PI’ôP¥x»w rgô[¿®Êþ+jŒWË*ZäK¥¸ö–9=3 ü êiþrãæêêçAéÅý¥ó¤Ò¯ã˜o}õcœÚžçÃ'ÿ--=CÜkcâ_ØþIîYü|óÓ¿ùUÔ&#ŒébM— L˜^°¼Æ|G:¯a3®xãÕ:DQª’>BVXc’‚h錛«w“$Xkó8€µÈ kÑý~¶îë8Æ$1aðÖÚ/3»±ÿp…œÈÄ·‚}/Üëcǹî¶Û¸¡ö7\~…j÷²çà!„P#ÏsúÉ¿c»ÕçÃ'–f2鵜 "ùÔê{yª{§žxcºÝÍ–ƒéyK$¼râ¿ û=¤R>‚é]`kÑq¯¤õLƒ”åM)2úƒŽL­vký$çCH…0÷½¡ã|y©Â‡ ©aDÒk#ú ‹E÷» ×U>Ï„Xåªëë%‰”ðÕ¥&§ŸÝ¢ÓqÁžÅ}“,X¤1=Sþ ’¹½˜Ûwˆ{ÌïR[Ô'od˜qÜë¹SOòÐÆlõôÛë¥ûî57Br¯z+ǪÿŽ^s: `t!aj&áHç›À»‘2p `¨°ŠNzôÚ›.ˆe RÈ<ªé×{£cçÁà=cщs‹øFgƒ]XÄðc€Rbk4×Ê d>¸ +áN\4L ‰îw±Æ„„ ¸9úsæ÷D•ÜÈ+J´N,ßüúiÚ[mkÝÆRï Ñçºí™{ú[½î8Ž¡DX!9vâ&ÖÎ}Lâ¥Ðð“X+è·–y¦u5ÖZT9Éõ‘:áã +úý~›Šêct‡Ô" *0.Vj™+hs/¥ ‘jÂç J¨JuHý !‘2H§¼<σ!{aëÃhˆÌ€ÙiHÀ(¨õàÙT@P­cŒÁê„Àl²W>žýõŒà%i¶’Gþþ+|îß²Ô¿"K²üÍêOpÇÌ_ñÆG?Œ Bö]q„"Uj\qõõ<ûØÃ%õ_”~!$¨jÓ…‹+5„åf € ÷¦ï—€®{o!Œ@‰˜©`™-»Ï£“-VZ¿*Ç2È[ž“ê<ÐóAzì~\4àû¥µÌgÚ¼ `­eV< ÀD£(ý2c†•¥6«m>|òçYêÎO%!øêæóµâéÇžFk?« ?püæöfbjž‘A!©NÌrlâ±±©`„àªÚßPD5‹õÄOK£Ûsê¤sgñá\[€­Û‚m‘ÎÆÅ[çw/,ŒYÈÒKD5•4îÖ–Ûœî]ÉéîÑ<Æï3pé¿û6Þ†‰»l,¯h·~^wûëiÌ,R"|á:W\s'¦ä@õ©Ì;!à @]nr÷ì11SÃÚ‚ôûá‚y‚†Zõ©n“Æ)ÃCnO]Do7ãò`€"#OäÓ2óô{P¿º¿××bžl]O£äé×T• AËÌзô{=†‰<:èS¼ÎüLÎç½Ç~…ë'¿î‰çà@ô]Þ½ïÿd²ÚfÏ¡zFü" uìže3^p¿›”3}Ï/ñá2„™d¬™ƒtZ†Ju˜hQE‰žû T¤±ˆ"‚ßMî°9Òmð6n¸óÝ<ñÐ'øqñtí‡8×;Ät¸Ê´Z¢Ò˜bß‘9-¬î•ˆýG+ý}îcA”ˆMCáÏ?ñá2b€QÜßµSÄÔi· 3óÃAŸF#äxã;°âà—b®ìPõQ"Ñbfa/#ãû… Ï¸‚WÝòNö½™Õ3s ¹Œ êT×RkX’x K?Ú[‚sýCldzˆ ¨î‹É°<ÿá\¿‚x‰ac—Çc«s6Ì:Mͨ Ï£ ,†Ïqãä}#'I‰„7Ïÿ•‰ê“…xÀÁ5ÃàR 1ºK¥^gï‘ë8tÍKX¼âõ© F;¿ßšaé7Z[‚ÖïÎb%Ùà²ãÖþçI!üÀ ` Ñøc &‰1:ÞñXk-ÆC´ðÑçz·³t¦O’¤·›ÿ«OÖØ{xïÜ÷~^6õ)¤ðù~,“Á2ïÚóïÙW}–·ßưt§iáîåÐ?ë¢|¦›eþláïQk?¶V “ÔyhãÎ\ç’kL’` É¡ô9²gxD]Ьî0™»Š~_K€ËféÌ’Eˆ&¯·õÓ¼å¹_eÿ³ÔƒjûÐU‡Y8°µ¥ulŸ%ªÖ¨7ndáÀ!„ &v1æ?&·=LNô’otYú-Ny­œ’œmïå37S¶>ïŸ.yš[Uª¨0B÷:YüCˆrŠØXãÂç*pÂ4F°-!B™69ߨ•°Öøh™Bª‚™½Ž‰¨3¨Ôüau‚îÖºß͘"éu°ÆRœÉ™ÑÄýÎI¯Ã·ä[¸M”ï~g‰›^Ñ`”»V­×9plD»W–ðÝeKî¡iÝÿÙaX;#èuæL„Û1 T]ÐÚÌØ‹&¦2 M+‹D+õ<2ØcìP ±‹D_ †å.ⳬÓaCËÙ«£¯dt\JûBÖÔq/c »L™ |²Äר°FÜÙ&šP|jëçx·ú9¾ýÀI®{é¨`ц$ºÿí÷Ðþ<Áœ«{£;Y¨7ý;Ït.ºMèlYó/¯ÿ|.P¬Ç{xxûÕ.=lg¹®ñ„öšA‚5—?b1`ìàØT3ê;R´‘ØÎf)3 \ÉŽ¼ë1ûÃv¦9@m ïÇ —ÃO1Kú:~ÿä/óúÿ¥ý¥'¹áöcÔ'‹ê}w=Ò÷“F¶¦5VòÍé¯Ôaî ;¥Îí·´Ö4:g˜þ„»íGXŠó×›?Ã’>D,Oš éPsB× Er8z8– Σþ½§¹+/À/ç;ÛÀ'™í0|tŠ|Í58ðd!#ÒT§ÄR)N÷®ä?}ï—xvû0ÿ•ÇX9³Æùˆ8*è³[ gnèusÉ7ìswï…ðô¨MBµaÍ1DuêS0½ö^i˜=‡¦žâŸ.ü/ÜXýF'¹:ÃÚBî GÍMìL–¡ßìf½ GúêFÇ€²6®:Gª`軤ßÅCX!ˆ;MVëŽ_l~œŽ»T&¦A’^‡NOò˜x5V‰ÖîçÌs+è¸OX­FUrˆ˜d2&„¥°¯Bæ^ng/“4Kp/£[Ž1FÎÖù? "Ç$ËQ¾Æ„Xã»›×"ê¯H Àô×"‹t&ýR¨0Ê5ò¨¥Ù8#p\ëTÐváø1Hqf„vðݶòCeÞX)Õ^Ýá]£—€¨ð™æ¿æ›í7s}õ ÜÐù'Ÿx†°>E¥‘¯ÿ‚ƒW^Ëü¾c ¯ýã—k“’ªÏ$?û,uk /1€Bz#ÿx`*&^à¥âÓ,yŒ¬ü2F; «†`2=‰-/Ë£–[Ð#‡c¬ó2@ÖiLÎß]{ô…¤T^C”`’¸äÇÊ0"no˜Ý™•&î9`ˆ”«PQ„t¥eAÀ9}=g›×ðÉçÞÆ^ñ7x–Šl“VϨ~8þóûŽ3ÒÝi'ˆ’¡gua HsýÖìNúwÁÕ¡SÏòº™?à3­ŸGXÊb¬-Ë^î]<+ÎÃ#É’ß„éâÐà+qÄyŒÑß©°B¿ÓÄ$qîîtÛXk]‰”au‚~k‹¸½M81 tÜA'}ªS Þ „hbšÎæ q§Imf+$&ÖÄýg+·°±ý†,HeŒæ¦‰Ï±¯ú;c‘rÐòô±&ü¢ØÆ˜Nirw”~?Œ«AÌvz\P©EÍKíçx¨ó6–ôÕàïÌàŒI¬p±å´ºiwÔ¹GLãZ;‚,d}ã"WƒëˆÑ‰¯jµ` ˆÛÛX ¨Ô²ãUTAÅ=zÍ Tè0oºß%¬M”Î)ƒhbŠ^sw±8» ¨Ô‰j n¥‚úì^Ú«gh¯E¨€~{ DÔf³3Folð½íì5´·6˜˜ÝÇh·°l¦ê>“|“º~ ’~£¡¹Ûë‘úí¢1¥™˜%w¯ýKµ­ É›&ÿ¿ûÔÿNuvŸ›!ˆÛMtçsÖïг!$Qm å´¦Ö$ýŽcf‹³›úÒÃÌUÊι³f¤?¶y´oPú]¿<å¬^ÿš.e®DÓ$½Ž¯ô•D‚¨:tÙJcF®²ÖªÓsDµI7AÆ8 6‚Jc„Äí-’¸Gmj ÖÈkê|hP­³Ù½m#š›«LÌ`XÝ——kú«;…`PõÊ^FH¿ŽaéiI7ŽøüÉWsªu+NÌ=Ê+â¯Ðíf÷›lªÓ1µ¨9Øš›fà1óVWA¤MTºJ¸²y)‘ ó-(Üp½AzTòH`\óÅüÇ%£±ßŽ Rƒ4¸ãqu‚(?.u?…”NåIFÖ„ÕzVZe­v¯Â'w´¤Ò˜!¬5XJ޳s…½;Åüýߥp¯i-…0LzƒùKÁ+[~Nr®½—÷?ö>ú¶ž¿ž[y·ßÈ{ÿ êÜ6Ó{ó8í Ѽrþ3<ºt¹ÃR…^ФR¡<†0•v'˜ãr2#‡lÛ_úQ¬+ðîO18”>´›—DqèÚ ‹—Ë ðïNö®ecm•‘F`1æozuï×|Ý)€#Öþaî¶Óÿßãÿ}Só÷êî[JÅRÿ(Ÿ\~½¦K ްj™ Où€.”‘™ Oh!Åe/p\~ £™ 0‘N¢ÿVŽøJ!UàÑ»g“ké·V1Æû²cÒ¿ÆgûF­ûYЧxc–NSðtó›ÉœgX•ß1¯â‘ÖtM¸Í§ BˆD—º\Ï@¤9#ŠlŠÁÒ}]#\ž ÃLàþ Õ)Ñeøî³¶-•✾¬¡µ¹Â¸aÙ×o—c0ïÂKi¤Ÿõ{гíƒyà¬À©ÚF(6õ":>‡ò+ëlpÚGþt.ù>‘d ±€!tÑŽË— ä~fP/¯ JË‚Ÿì\øuRJ6ì!b&hm,•~ëý}SP÷©FHݾA°Gö2ðyi¦£ÍåÊ3›¤€Õªl—@ÿ‡T`…bÞ3@Jä,œC.€ôòf€t hƒ‘ËBÊÅWéÚ½Ÿ¯¦¹¹Ä¨Ìa.ý’Ÿ„ÃP¯Áa ¯QÕrõô#ÔƒvÆœiØU×)l>:Ë´:GTq^Opce™ø#Ä¥Z‚ï“^ CÁ¨#dI$™1D>ù’Sñµl¬c8vÜ1ú€ÝK?ÀÄ RsÏ?G ›?}%Þ¾økˆ@”› Vâý>åã%ßKÿ`Á:F‡‚m±Èp‡h“1X“¥nÇ…‹FûRoF…Ôïàe¯ 4¨¨::‘!\Ø3î¶1Ißž|bÄâoÁú^;¬IØèÍ· ž@ô츠OAÝ—á^ÃÒßï@sC Ì,Ú’ôƒ;õì>Í­ö+4‚ >wö­,éH¥8Rý6ošÿ{+Ï0»_gÓU”þ¸ë°+ýƒ#çÇêëmœñ#íexþ>C3슥i¬ñAˆaŸ?éuxÓ`„Dõ©¡ &Ý6½Ö†§ÆjÓ‹¨¨ì¯&ý­•3.óˆk„PŸÙCer¶tœIb6Ï<‰îwRbtL¥1ÏäžÃ€ Z`ûܳt7WIz-n»éÏ™Ý{ åµß2èçz ­MØ\“ØØ°ÔœaOcƒ(‚Ú4% ¶ÖEõ[ÄéG¹vêÛ$&@IÀÔ$“ó™ÆkŠ¿Ú[ŠG›/£gjHä‰;ÍlŽeÖ'©NÍÑDÇ=t¿ç´†H©k¡ãÜ}úúË¡ÓЯ÷K±|w!×Ã/¬N ƒk ýÖ–Ãú5fòã’>½Ö&aµA41 :›+t6Î1±x0ÓÖhZ+g¢*õù}!èl¬Ð^?‹Š*¥ÄÑÖÙ§ÁZæŽ\ "z[klž} ºppþ,–Jc–Û>Îbõ,G®ÿQŠ1£ ’¯%¿‹5}zmìlm ´•Í›Ä×Ù[3¨¸Ë  {i’>ô»‰ Ú…>ñ“Ò}ÀŒ;`ûš¯»3[ÞÒ9Ra• Zweóä†qIëúȬ Âï$êA%F'¹õ[з®÷ŽñEÎUº)ü3jr‹C×ÜEYú|}Ý¡½¹Jw»Éÿþ-|é¹[ˆi ¼F’a„ "TXñÿ"dºDÑ‹üáÊÿÃ}[ïbë,=­h­AÒg<#Xèµaý”dë <×¼’ß;ù³®Ò¶Ö…¶³(bNø,–ÆI±Ó(. ßGeÐNV@§ÞCÄItƬ»Kz8d‘;¥ÂZ3£×£ÆÇT…7\#šÜQû¯¼°2YXûué+¦}Ï>³Ä3­«y|ý(B:|£”#²OÁæR(Q[ÆŠ½|¾Ôþ)žèßÁÕæÏ¹yþÔÖÚÔ'ç¹ 1Ðï*l¢y|ã*¾Ò~7'{×9YŒ°%Q*t×V*Ïoˆ¼-Mž±-„JÃdø„hQÆ-™aèEXc¡ÌŽDÿæÇå¢Á- fÜq"ïœ-U5–.ª:WsèÚÆc-·HEŸê•äA1$ùÖti®-Óo·ùôÒ!„Ã*féWå˜Àåò.§‚‹£®µ–³æ%|÷lƒ=ý.n<ÞçÚê—¸ºw?`é&Z½ˆ8X`KÏqÿÒ+xfy‚ɽG|z×…ŠEß·É"Oü ßÈó ¹(Òu F/Y³)Š J”ñÿ©¦k¹)n.É»{“ï¶YªNQA¾@z>kœ YðOUÑkmfèãtÄÝ*ÌÁ#ATÅbˆ»-‚Úib¤ßj®Õ|Õ®ñ²êrøêW„õ‚ô'¹äþ}v™Ç›7qª{B~ׯñ^ý{pDH×ß”AÒº;‘¹ÏÖÂZƒ~§Ésý9™ÜÄg·ÿG¬1lŸ{½£b™ IDAT£c¦ö ¡g]³HuR’ØŽë•ä‘Uy^#5…ççܼàÖÑiÁ¥P° ­æè\O0¸Œ[¡pCz?³X¢”ô;!Jž°RÇ$±[›S¢¶›Xk‰ê“ÙgÑÄXK¯¹Q"¾îw©AÑÄR*:ËéÝ¡ã½íU*SsÜ~€0 ØüåŒzÒ¾›ËK$Ý.Ÿ]ú±l›X!UF|÷šc Ò÷N+ðP›Ý‹T½Öfv¼«þíS›ZȼhbŠ R!î·‘Aà?®KZTŸDÞœøn (HÿÒ=R.´ltÙ~s“ÄY„Ïb‡\9U0:¦ßÞöj9A'±wÏ ëK¥FÒïÐÝ\EEU¬5$Ý•‰é’öPa…Êä,Ý’žc¤~k›°Ö(–„4±¹ô ɳ¡Âˆ^s¤¤>»—K¼´ò§¹öuHUÉ‚>% §wý´î°ôÜ2¿v+>¾‰ßÁ˜)VŸz!f^ImfR°úÔ·²9BVŸþ6XKuz‰ùýXk «u‹‡h­žÆ&1B)úí-¢Ú$µéÅ’šž˜?@{}‰öú2A¥†Ž{P›^pÆ ·;’n›¸×FJ×KÕÄ}:+ ÕÉYçAy/)î¶¼-ä–ɸÓÌh–ºç)>Á Fñð£}âòíÑpl@HÂÚ$&î9Ë\…TªõìÅóU§æˆ;m÷pRR›YYÚ\›^ ˆªÄÝÖhê³{ñ®]™œe&€´ß¥6½HÅ7xEô{D• ö¹•a g.ùÖtÙXZB÷cþvéT§S)rÕ/éôjØ3Y¶ûçà0‰*@øÂΉ…ƒ„5‡t6Z31wÀßcÙtŠ‚‚¨FÜmaŒ¦Ò˜Éƒ>i?B!QQ%HÇÔÂG ‹ð=!%*ôuÙÒPŽ(¦Cá°ÈJœ¿/S÷Uv> @Ö&kç=2¬5ÆÆ®‹–Xð•Å&ë)8eŸæ†ð½þ‡2Ì×þ è™}tÜäÜÉ\ Mu„ɽ³ºƒÂ¿ÏILÈÔþ£™û•1fFQ‹5”CW&ç¨4f)¶½T¡‹ígnÌÓÇið'¨Ô¡* ×½î ©F ׸1 ¾\Ç€Û—z!ÖW^Zc¸³ú;T&æX8ôÆ=Óe`mé,Ib¸wõäØÂFæþ€—YWPYl¥Z X„tÍž¬¤ù{÷^"l~\q n§“2WæêÞ$þÙÚÝŒ Œ´`2]ªÄã\ü-ÇN¼ !TAú{0ï6ºßbùä:_]}M3ç¬yÚ•2õ¹U†(&þ ñ%òzÚØ¼ÉƒOÌ‘vñqÕü·Ùir|Ã8»_\<âˈ¥ß½8¸«ö[Ô¦ö3·ÿz†ÖþÌõs@ÏÕ3§‰µäËk?LY,¤Ì|þl:šøƒê7߇0%b´á%^0Æ•kÂz^Œñ_d©/ŽËŸRlEéÇZöɇ8¢îãØ ?›©4èÓz&ý&«§7øÒÊеÓ>ÎïT}æë{ÕŸ`)uǵ·7I[¼´‚{Œy–â9ŠLPx<ÿÅ$>¼€\ú‹UÚ]ã5õߤ1w„éÅ+)I¿n=»¬œ:CWWøêú[2Ÿ?‡—çp²¬i³ c¨ñG!“t¯ä ZaT–uäGôq×¼ãòÆ&Ø’W¨¯²_~“£7ÜÍh g'3þúÝ-Ö–6øÂòÛˆ©»°«'tj¤Ñ¾\úGôDGˆ1˜EQ Úìð/í<>Øèrð¼{ŒÇfûíîpñô8ØÑm´~Ó„kq6hŒ+n,`KÒï—£âö6wÍý'¦÷\Íä¬Ûpè™Wú¬œ¢ÉŠnŸ‰»¬Ÿü×V¿ÌââS=ñ^†žii—c‚^{‹ÍåMþfé=h¢Ì°C:ëçèn­:, ¨Ô§Øsííî¦Àe6N}/Ï‹ÁÌákKÑJ“ôÙ<óº×õB$˜9tµë†20šË'}·—¯Í,–ΕŽ^sÃwRsµ‚a¥>„•Lé’ô»is!„T¥{q¸>ñ ÄsRnÛUk]ŽÝíÜU†J ÂJÞrÄÐIŸ~k“°6YÀ.ÓÞ8GcñP&ÖhZ«§Q•óû~²›kg™ #Ï,©¼uæi¤Iø¡#Ÿdþà êSûz–á^çž;Ízošol¼Êñ •Œ%jL»ÞëKeé³–´oo¿ÛdãÔãÔgö1uðJÀ²yê 6N>ÎÂU/ÍæÇa*C*sÓ£én¬°Ó¨4f*(ä7F RË’fc‡ÇsÊ(p»”îpœñ /¿(ný’nUbu¹é`ÚøÑ©ó²”‡Ã* tkYer¬ÍbÔb-õ™=Y²£:³ˆTÝ­ÕLúãN“¤×åöƒßbZžæŠë^Kè™K~ÑŒcÍt´Å›öþ 5Õ*Ýc}nSûŽRŸÙ“mËRŒØ¥¾|{m „bêÀ1Ï;‚©ýÇJÄSa…éÇ©Ïí+÷÷1‹‡¨M/ŒÔÅQiÌPiÌ”šjŒ*ªV'œÆÞiÉ1é¶}ªÀ¾eÈHL`aM¹1„$½|c…&°`ø…Õ:q§E*ýýöRô¹{úÃì¹âfªsdHŸ gjÓåÀñ: w.þ-?wÕÏóÊÙø—ÅÞýév;Öæ=ÓïzÍu·Ìæ'…]÷w’ÊÝŽ]Úy»ò&Î3\žÇ±ñ(¼Öˆ÷blcì§¡ÐFˆÛ=+‡„ÙíäSÃOª×:ÍG&1/›½—º\ã𵯦ÜÜ¡¸î—AØ.YÍ¡k4 ‹]Þ°øþÕáŸáÆúߺŽÏå§Qò^ÈyY¶‰û¨0ôÊ¡  ƒhó0jþÎ?.€°»t)Çeóë× ÌN²C)Ò¨ å˜ÀßJYÒ(Å}ôJw)¤_¯zXcºÉë÷}‚Çn#ªNåAŸ´Ÿ¯.Hþ@cgw/0½®2,Îmò¶ù_ç½{~†£Á×2ƒÖ1€.1ÉPÑéò–m¥©ât\Go,£äˆªÝ‚Æú¿ãŽß·1†(}tv³Ö^1ý ªAŸƒW¿Š2г5¢¨3¯ôì詘Ûo™œ‡ÊÒ)~,ø%žë¿„®¼…3+Ú Öw*•.ýšoëYëVcfqJ—ÎÙ€W¿Ã¸t¾þð¥òk•!aVàôC!´éÕÝŽkψïÒtæ(ÛÁZS‚v é@%…#ÀZ´öûä Ih׸kîã¾úåÑDAúG=*}Ü5³3g^ÁÂ!C¯êÜwøW×?Ì7×oåkö}l'q«—ÍîÑUÛäMœ±mÜþ¿ãçfüWCÇíJµ_È wX¶íHŒø±…è¸ r$&Е%ÝÊ´”ÉX] úè^FX£¹=üa ØåäA9Vòó~¾Éür>¢ì=¢™=7->Ä{g’×VȬe-[UTuÐÒ´K'Ý*ÍÁ\&w/ÙÏ—2…–©*2öŒÕYVìBGP©at⢀~¤î_øÖ'±@¿µE*ýq·…î9|\en«ü)G®}%*¨æ†ŸMòNޠ̄ܥ+ü~` W°ÿ¸ffæÖ‰òÓ3ÿ˜›Ã?ÁhMujŽ$éÓÝ^Ë<‡ÎÖ*ÖhjÓsã'aר½ ì]ÐBìpÎÔ0jç6{Ö”Ûµ&)¶sõç-}ƒ1–~kË¡u*õìø Ru ÍæzÞ'°×!ªv ±nKÖ¨1Eokݵ‰CÐoo¢*5Âê/~“°Raï±ÛzæÝ<¬éÒÙ^§µ±J}&†]HúA‘êÓP›JØ8“ðZÞÏCëo'ªMR™˜fëìÓô[nÏà~g›êÔ|Åd-›gž$íç#€æò)׊ª4ò²ïÎÆ¹ÌBÐkmÒï4B01 ;.î¶Hº-¼Ñ‰{t·Ö@@TŸÊ<(·A‡ÃT¦ÕT.P'2¨[Jg)ÔhHXZÿ—?²ÊEvÿImbg¸•Š Ò˜ö¥ä}¤TSs¥Vpø_Ws¨ BÜmb“„êäA¥Î$'¹)ú8G®»)£,è“öó-ªtxö±“ؤg$³{\ÃÈ`¼ô—?ÎF¯«x¸ó&Œ±a˜Ü{˜~k›¤ß©˜ÜsØ•i[Jíô£‰ið!í¤ å ‚JÝ¡¤zPÛJ¸9+áð‰¦ròPuwé¼Wà`‰žëï(Çõ ”;j$!jW{Ùº‹•úh¬ÚÀrTjQ%Ãùpgõw‰jÓì¹âfÊéÞf.ù^ݯ=ƒŽ5¿ñØÿÆ-s_ã•æ l¬ÀÜ^Mͧ*Î'ýého€Ö‚/oÿ8VhgÐ ×°ªæITjÙZ›^ØÕÌ„µ;ïèãFZ–v¾‘j™ÝŽÝu ¿Äc'œßœx‚ëÂÏpôÄ"DPúô‡¤?é7Y>µÊ}+ws¶{˜O9ÌWWßÀ÷”›“¯£V$s{µëËsé·¶Ö_ßúšÉ,R„ôûü¦ˆ¤ ã…à Lâ ÎÏZÍ]Õߦ2¹‡…ƒ72èY¨ôY;s†$Ü{îÒNa›zvê_ò›Oþß]»†åg]ϬNeŒô·Ö!Ñ÷m½ƒâv¯ƒ=z^ˆ­^/öxA5ÀN8¿½â;¾Ì±ÿ=åï гKÒk²rf/ž»‡¶žÊ‘=þõ\ÿ(<ù¿r¬þÞ²÷¿Ðo¢6 Só6ëà’ÒhØ^W|eãGé˜Iv½Ò½HÇ ÃçÁùY«yuýýÔgzû¤¿ôl³zú4ýDñåå7`]yYu‘{¦w¿õÌK¸aò>Þ¤ÿöæ“s†Æ,™.l­C_W¸ûG2#+Eõ”ÖúÁ÷/Òñ‚-;áüÉ8¤¾Á±îÆY´¹ôîÞaM„%’=Þ}ìýì¯=WsÚ´*ýB¾Ó~5¿þ̯ñé•Êêj3O*Z®ÇosCòÅDL#/Æ9Üì|´/%|ëRŒ]ìt‘ÇŽ8?Ç?Rÿ%ö-NùŒ_¾ÇÑ-LÒDë&&ÙÎöñ©Ôb*µ˜j²ÊmÓŸg¾²Ä™þqú¶‘aýäðpº nÝÖ²`ž ³iéè)>ºú>QÞ Aå5Ù¹Š= ‹ Ñì°P­ãŽð™³b ÿèãL¡|<ÌÂÔEœŸ5£cŽÚ¿a|Œc'~Š! §n—€žEO ªõ9x¥kçvSðun˜ú:÷o¼…/­¿ŽvTH–ým ¶÷n½‡›oå¶Æ'y¢óRŒ¬e…"y­€¯ÆõABæf°øÌºßËÂÈ#{úyÖqŒ5IV’vþù–C~ýà9ÝÜž™ÏYÞ»œ¹KG¶4>b(Ü¡ƒÇê~—¸ÛÊ¢‰I÷ ™ô’~—îÆ9^sèw™Ýw-3(ÅüG=Ó=Š=ýêSP›Ô4×áâ“Ü:õ>ûÜ›x`û­È™ƒCRÚooÑkn²•ôù¯FE5&÷¹ áëóuÜ£µºêz!R2{Åu#s$ºßsÕÁÖøL§¥:57„÷³FÓÙXÎ[ßXCP©ûBÒü]©|ÏÏc4GÕP»î`>c)„êÇ8@à@¤ý4–rã«Ýž<*¬dyü¸×!éu °%£â^ÛxÜ6±½ö&½íM*Ss¤Õ2F't·ÖxÉä—™ NsäúAèÙ.=˼; vôD@cêÓ†íÕ>o•Ÿä5É—øüæOòíöÝX¡2F¸ndA4O¿¹‰P7T/€kPázõõHºî]˜3k-­ÕÓÈ dbþBJzÍ /oúÜÝZÅZC}nRè~îö²Ó /«°‚ ‚,T>ަ©Í3ª3KéPë#º;… ŽH¥XúôÁUekx:2UéÚ « ÒL_ôIzm„yÝܳxøFj“{Ø©£gîµS?_!ajö_iØ3¿ÉÌÿGþÅþŸåXåü+šœu¥è“³È0t…¡¥BQ÷œa}’‰…ƒT'çòí묻pQýÇ&6íyèÕ´Ûæ&rØÇt“÷‰êÓ¥=–‚JÍÇï иZƒ°–býÆ'—dàú¹óí ú³ÅÝx©º,@†cþø^Áø-Íü¡i™u„T %Úw±Æ {]n™ür™Ã×¥P¯A ç`K÷¼£géÙFN ÌìƒÅ£–ýÓ§øñ…_æŸ,ü{Ã'ñÒ’l©@*„ò’ïKÇÒ6ïYÎ>M§k¡XÅaƒ¡ÐmXqš#i×”AíiXLòƒ¡ŒÒÀyØ¥8” ur¿¶fÄNíáßåA)•ïÿç@š6¯ýˆïé—:åÃ[·—%wݼ‹‘¾ ‚ùƒ†¹CplòþÙâÏò–éÿ€°yúTʼ?BÚ’Eø%CdM(¬”X‘Ãr4Ã7ªœ)ÒÙIÛïf™Vg<ËãYJ¿ؘÂÓ¥ùüC ’mäØe ˆòïGœÜ5jvGŒ¶Bó’icò Ï­§¦ÚyO¿4ãgºØ‚¡gL—NsƒågÏÒk·ÇJÿn ‹Whª“p¢ö9„Ý÷€3]CÒç"[ïÝãXÇ46M3ŠŒŒ5e(Ç ‘Áʲù‘iM5Ì8¶3÷£Ç˜GBÂvy„{dJp‘ùz#‚>Øl2"»Í]3æ{ú5ò O èYîèÙmõØ8§™˜é·ÏÎùÀÙŸþoc ÓTÜ¿ýv´Hk\))%}|.9ížæ¿!Õk™1`óçNaåù<-™®Ù 39"}["Àn8 ÀÇ;Ý…‚Å TŒ¿ìÁÒò«¢\ÚÜçÇ·…¦¢ŒïéWz¶‡ÔýöÚ2q»Íûy{jK¼õðÇin4™ž74ÜR½+.䛈û6ßÂzuîçͤÊ@™æë™¼û›ÏÃI(ƒK—"_%l¬ßN¦‚ÁÖxi9ÙÎ ²›±;Mq¹€Œ>… µ†õ©M‚´•<èãsýIÍ®r{íO8|Í…ž~)Ð3ߦ=5úÎ>³Ä7—¯ç¹íœêå[·òÊ=_äuæ“l®ÅÌ.ê³Ì?Jú54×%_\{=]A(´N‹Bp kÁ¸@¤[Ñ,Âø¾€érgM&åÖ d÷:¾kzI½Ž]‡u&ÎQ)â×Zë£c†¼¬òAÂ(íéWú H¾ëéw–¤×ã“O½ÁuPá‹K¯çÿºï}|ñÔ]¬.)Î|OÑÙÊou”ôo¯B'™àþ7»Î£ÞÉ{PHM“j,·DX«3ÒÃÖøÖDuÐïlû´¶«Ã3qÏ•Šùó§1çדÍñÕTc5À.À¡äÕ˜ñ}e…X¡1žèi"g;(|¹sºŸ­5Ýï Æ\â¥ÕqäÚ×ûž~〞¾§ß³Ë|cõ嬛#˜¤EÜÞF!6‰I>}îsÿæ=¼qßǸYßZ•ÌîÑÙÆL©ôëÚ›‚Ï-¿“Ø„XÝ%ÞjúM2$ÒZ+§JQ›œ#¨NJpocÕËï­ÜZ9 ÂêD^ÿ(%a}’¸½‰ÐqD„ÕZiiˆêS>¹‘˜øˆjq¤t¤Æ›1Û뀠Ä,6¶¬¶Ó/1;ä(¾ïtpºÌ,Ü ÐO°àþQÍ×ÉÇXcŸ ÜYý}¢jc §ß¨ŽžÚ›k˜¸ÇR{* ‘Á´›, ªZG…U„TlÅ üÙsÿœ//¿™{ö„«úÖ%Ó &ë忽*ØNfù»Í×:#\¹˜~ÖZ…yì_I/éŽàAµ60‘>š¤¥ïB8†2pMÖTDÕš_È0¤Ò˜ñ•¼N+Œê% ¥‚Ö/ïTBIwpÎE7fÌøðçúáûùû½Ö ³öINDŸ.ôôón_ôì”üÿJ]29Wå­ü%·->À'Nþ#žhžÈ ©ü>œÑ¹Ô?ÌžþyŽ7á­ûÿ+qû$ÕIAmÊÒݶ|vé¿Ãøµ8$ªÔGôåU¾€ÒÙ:‰¬5 ®b6 þ1‹î/È0DEiÓFáŠÔ+Êc)B)U/Ÿkpž•B± ‚†`¹dé`;h}™îõÆê¯°ªÅ•7ÿë—¦{Ûì™4Ý«n¡“&ÆlSmÄÔ&ûD¶ÍK_ãØäw9Û=DSÏnWÜ3)ØHöðÀÚkXéïc_ð¶Õa¥¿ŸOœû)òà*ëÉ/UºÇoÚ&¦(Ç3 mú”ºŒ”?äï­s»)•L1$ÑѺÛ帴ˆ ‚)žV{äã\ÞËÑï$íÆ•=; Æýó¿6aöv¥\ÁÙ'86ñoyxóå|öÜ»ØLödª äŠooßÉ£­WpbòNv¯éóüb0Ú'³h_VÕ—eÅ >úT¶È>êé?K{õ œ)é¥;—÷*°"‡”ª/¼Äã’C†€“Vc‘<ý/"DÄÜþŒêç[Ìÿ»o®S*5ØwTÓÙ†›Â¹aúAî_{÷®¾Žnæ0—,#B¾ÝºËzRdj>SùiT.•úA:Œ ªˆ"Cä§Ì`­ÈŠ_…ô;ªé Ÿ‡!åÏ÷¸øKÀR6÷«­54õßéÞM5y–þе¥'©7¦ £À©{¯úâ'_²Så/„˜œ³ʲÀÓ¼|ö3+8Ý;êÖø Ε7{,7‚,cs(YªúË*ú¼Äñuª4R†(5wN#§E£íyfˆçæÃ¿©5m ¤Î£í;y¼u Súú§ÿší³Ô&j—à^&i2Tà9ð‰ªÐ˜µ(©9(å¶™/ÐÑ“,õ- _èZÚ”©¼×ï5=bŒeŠ!;±&Ïl•‚!þ¡0ÀФ©Rï?[ 4&a;™á¡ÍWñLë*æÌÃtÎ>H¯Ý¤R“ÑÍ4BzšÂËàÂ- 3–€Ç‚opãÔ}l${YOPÜL:uùJ ¡Ót°H£™f ÚÇ/ý#'¼È9e¤ñ?$N:-àBÁÅœ€cˆþ÷¯½†åîø6íå“.rVí#„Þ1Ý[üÜâ4~µµ)m›ë+_âtï*6ÌÁŒ Ò-Ü÷áRŒ&~ö éµìÐgçg€\«ˆ/$\2#Pà¬[Œu‘+­1&ÉóàJ Àh¬RHððÆ­<¼r·Í~‘7%Em%ff&gñVôð÷™ ¡Ö0ôš‚s[UÛ!¬7 pLà¶sq-)C¢ÆôÀÃ8¢X£]„Ϻd–”Q}r(ý›Ä jŒA` kÒ)ø”¯N{'—Œ ÂJù¸ô”Þ…NÝÑóp/džÒwvÏp¥ï(-ÐÞ£-÷nü¸ 4IgÝË‚ô é¶¾IµBÏcöd!¥ÝwÒÕ¤¡ÚH)zí-´Ž³­Z„TTÓsÙ^ŽªNkÒõ?¬N8¢b1ƒÀÖÂ(AÊk›s±Æ¥a€ÔåñÉ‘TmY+êÖ( >Ià°ƒ‘ë!lu‚Û^XC7 øô¹wóµÕ7óº=ÊÍÉ+>Ñ3ÁÈt¯µ°µ¢xpýul&óÑ¡p^aíÍÖh‰`x}Gm ]j!Q~Ó,ÂJAºó×l›×lÒ}KÍØd‹ÝÂv;.­H#aà’R¹6¶2Dâ²VB ¬Š2÷ËH…0!ë"ƒˆ-½?=ù^î=ûF~èðǸªÿm‚šdfÑ”ħµý÷®½Ý\zÉO‰Ÿ~, ‘»kÈÁ)vO¡”í“«r!ìX#{æô;›B̽‘™˜Ê'‡EØm­áÅ5/¢`ݤ—Ù²ÊÎ%3d`­¤ë!¨µ_6|bE)¬tû¯&GùÐsÿ†+ªò–ýLÒyšJC0µ`4ÌÀ֪⾵{h™YßO]<—uLså=,B ™¹òÇÈ"9Þ{¥ï¥”N›!\s¸œDJü,ÀTdÆßìfZ/æ¸ä ] ¬Ïªas¼›@8lIå%OF`¥3žkѸæËÆh”µèĵA9ßÈï¹´ã…iSä|?Q®}©OHW]£| wá&Ï—[Ë@eµ…R(DZ4YˆÑ;$²uk¯ ³Àð6A*ýÖj0©Äl¥,ËaÖ|8ÛEíT!ƒ´³(ÙxâÃÈN|CKŒÈ¼Ãç{xaÛÄ L„°¤E ò¤2Îs°Æ’ŒÂ … "O›KRŠËHAª¥È_ÑÈó>8 Ôð½”o3ÿ.Çæ ¼0\¼`Ïû­ûÅñ‚÷ Fj„´¢FXß™ÛÇÁÝ2!\VQ:.ÒFÙ9 “+„7ô‡‰Ÿ]¯tç!Ȉ¯w\½ŸçìÞ…ŽËƒÒ1È8Á¥“óôiºQs†4‘Y…ó 2ÂN–ø.oq7{™½8./HGÁXÌâ)…'ja^1P¦6xž¢ªÍ4 ÃÌößï‹p\ž ŽAFlf“‰Ü}.¬ÿ#NRz‘ƒÕpB¼1¡ÇÿÝv£Úiï™SIEND®B`‚passlib-1.7.1/docs/_static/bb-logo.svg0000644000175000017500000011166113015205366020732 0ustar biscuitbiscuit00000000000000 image/svg+xml 0111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011011101000110111101101111001000000110110101100001011011100111100100100000011100110110010101100011011100100110010101110100011100110111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011011101000110111101101111001000000110110101100001011011100111100100100000011100110110010101100011011100100110010101110100011100110111010001101111011011110010000001101101011000010110111001111001001000000111001101100101011000110111001001100101011101000111001101110100011011110110111100100000011011010110000101101110011110010010000001110011011001010110001101110010011001010111010001110011 passlib-1.7.1/docs/_static/logo.png0000644000175000017500000000135312214647122020332 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.7.1/docs/_static/logo-64.png0000644000175000017500000001026712214647122020565 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.7.1/docs/_static/logo-128.png0000644000175000017500000002026712214647122020647 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.7.1/docs/_static/logo.ico0000644000175000017500000000217612214647122020324 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.7.1/docs/contents.rst0000644000175000017500000000033113015205366017620 0ustar biscuitbiscuit00000000000000================= Table Of Contents ================= .. toctree:: :maxdepth: 4 Introduction narr/index lib/index other * :ref:`General Index ` * :ref:`Module List ` passlib-1.7.1/docs/faq.rst0000644000175000017500000000705513015205366016544 0ustar biscuitbiscuit00000000000000========================== Frequently Asked Questions ========================== .. currentmodule:: passlib.ifc This sections documents some frequently asked questions about Passlib, and password hashing in general. But it also includes some common misconceptions, as well as some esoteric and infrequently asked questions. * **Calling** :meth:`PasswordHash.hash` **multiple times for the same input generates a different result! What's going on?** For all the hashes which include a salt, :meth:`PasswordHash.hash` will automatically generate a new one each time it's invoked. Thus the salt & digest portions of the resulting hash string will be different every time. * **Do I need to provide a salt each time** :meth:`PasswordHash.hash` **is called, and store the salt separately in my database?** No. Nearly all of the hash classes :doc:`passlib.hash ` which use a salt will automatically generate a salt, and include it as part of the hash that's returned. There are just a few hashes which require an external salt (like a username), or don't contain a salt at all. These generally aren't secure, and shouldn't be used in unless you already know *why* you need to use them. * **How do I decrypt the hashes generated by Passlib?** *Short answer:* You can't. *Long answer:* The hash algorithms in Passlib were explicitly designed so they are as hard to reverse as possible: you can hash a password, you can check if a password matches an existing hash, and that's it. Unless it's an ancient algorithm whose security has been fundamentally undermined, the only way to reverse a hash is to use brute force: hash all potential passwords until one matches. To fight this, one of the main goals of password hashing is to make this search take as long as possible. However, if you really need it, there are programs dedicated to this task, two prominent ones include `John the Ripper `_ and `HashCat `_. There is one single decryptable hash in Passlib: :doc:`cisco_type7 `, which was deliberately designed this way; and Passlib's implementation offers a convenient :meth:`!decrypt` method. * **Why use** :meth:`PasswordHash.verify` **instead of hashing user input and using** ``==`` **to compare it with the stored hash?** There are two reasons for this: One, :meth:`!PasswordHash.verify` uses a "constant time" equality check internally, which mitigates a class of timing attacks that ``==`` is potentially vulnerable to. These attacks are mostly theoretical for modern password hashes with a sufficient sized-salt, but it's better to be safe than sorry. Two, many hash string formats encode a number of configuration parameters, some unhelpfully allow multiple encodings of the *same* parameters. Thus, to make sure passwords are hashed correctly for comparison, you'll have to parse the hash string and pass the configuration parameters in yourself. :meth:`PasswordHash.verify` takes care of this transparently. * **Is SHA256-Crypt the same as SHA256?** **Is MD5-Crypt the same as MD5?** No. MD5 and SHA256 are cryptographic hash functions, which whereas :doc:`md5_crypt ` and :doc:`sha256_crypt ` are complex password hash algorithms, containing a randomly generated salt, variable rounds, etc. They derive their names from the fact that they use the respective hash functions internally. .. * How are the default settings in Passlib determined? passlib-1.7.1/docs/modular_crypt_format.rst0000644000175000017500000002510313015205366022223 0ustar biscuitbiscuit00000000000000.. index:: modular crypt format .. _phc-format: .. _modular-crypt-format: .. rst-class:: html-toggle ==================== Modular Crypt Format ==================== .. rst-class:: subtitle A explanation about a standard that isn't .. rst-class:: without-title .. seealso:: **Deprecated (as of 2016) in favor of the PHC String Format** In the opinion of the main Passlib author, the modular crypt format (described below) should be considered deprecated when creating new hashes. The `PHC String Format `_ is an attempt to specify a common hash string format that's a restricted & well defined subset of the Modular Crypt Format. New hashes are strongly encouraged to adhere to the PHC specification, rather than the much looser Modular Crypt Format. Overview ======== A number of the hashes in Passlib are described as adhering to the "Modular Crypt Format". This page is an attempt to document what that means. 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's no official specification document describing this format. Nor is there a central registry of identifiers, or actual rules. The modular crypt format is more of an ad-hoc idea rather than a true standard. The rest of this page is an attempt to describe what is known, at least as far as the hashes supported by Passlib. 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 used by various formats. The only hard and fast stricture is that ``:;!*`` and all non-printable or 8-bit characters be avoided, since this would interfere with parsing of the Unix shadow password file, where these hashes are typically stored. Pretty much all older 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`). Many newer hashes use ``+`` instead of ``.``, to adhere closer to the base64 standard. 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$`` ``$2b$`` 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 (as listed in the previous table). ===================== ============================================================== 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.argon2` ``$argon2i$``, ``$argon2d$`` :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]_ :class:`~passlib.hash.scrypt` ``$scrypt$`` Passlib-specific =========================================== =================== =========================== .. 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.7.1/docs/history/0000755000175000017500000000000013043774617016750 5ustar biscuitbiscuit00000000000000passlib-1.7.1/docs/history/index.rst0000644000175000017500000000120213016611230020561 0ustar biscuitbiscuit00000000000000.. -*- restructuredtext -*- =============== Release History =============== .. rst-class:: float-center without-title .. seealso:: **For the latest release:** see :ref:`What's New ` in Passlib 1.7 .. toctree:: :maxdepth: 2 1.7 Series <1.7> .. toctree:: :maxdepth: 2 1.6 Series <1.6> .. toctree:: :maxdepth: 2 1.5 Series <1.5> .. toctree:: :maxdepth: 2 1.4 & Earlier .. rst-class:: float-center without-title .. seealso:: See the `Project Roadmap `_ for a list of future changes that may impact applications. passlib-1.7.1/docs/history/ancient.rst0000644000175000017500000001026513016611230021104 0ustar biscuitbiscuit00000000000000===================== Passlib 1.4 & Earlier ===================== **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.7.1/docs/history/1.5.rst0000644000175000017500000001411413033503425017770 0ustar biscuitbiscuit00000000000000=========== Passlib 1.5 =========== .. _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.0** (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. passlib-1.7.1/docs/history/1.6.rst0000644000175000017500000004340713033503415017777 0ustar biscuitbiscuit00000000000000=========== Passlib 1.6 =========== **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. .. rst-class:: toc-always-toggle **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. **1.6.3** (2015-07-25) ====================== This was relabeled as **1.6.4** due to PyPI 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.0** (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.binary.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. passlib-1.7.1/docs/history/1.7.rst0000644000175000017500000003351013043740653020002 0ustar biscuitbiscuit00000000000000.. _whats-new: =========== Passlib 1.7 =========== **1.7.1** (2017-1-30) ===================== This release rolls up assorted bug & compatibility fixes since 1.7.0. Bugfixes -------- * .. py:currentmodule:: passlib.hash :class:`cisco_asa` and :class:`cisco_pix`: Fixed a number of issues which under :ref:`certain conditions ` caused prior releases to generate hashes that were unverifiable on Cisco systems. * .. py:currentmodule:: passlib.ifc :meth:`PasswordHash.hash` will now warn if passed any settings keywords. This usage was deprecated in 1.7.0, but warning wasn't properly enabled. See :ref:`hash-configuring` for the preferred way to pass settings. * **setup.py**: Don't append timestamp when run from an sdist. This should fix some downstream build issues. * :mod:`!passlib.tests.test_totp`: Test suite now traps additional errors that :func:`datetime.utcfromtimestamp` may throw under python 3, which should fix some test failures on architectures with rarer ILP sizes. It also works around Python 3.6 bug `29100 `_. Deprecations ------------ * :class:`~passlib.context.CryptContext`: The ``harden_verify`` flag has been turned into a NOOP and deprecated. It will be removed in passlib 1.8 along with the already-deprecated ``min_verify_time`` (:issue:`83`). Other Changes ------------- * :mod:`!passlib.tests.utils`: General truncation policy details were hammered out, and additional hasher tests were added to enforce them. * **documentation**: Various updates & corrections. .. rst-class:: emphasize-children toc-always-open **1.7.0** (2016-11-22) ====================== Overview -------- *Welcome to Passlib 1.7!* This release includes a number of new features, cleans up some long-standing design issues, and contains a number of internal improvements; all part of the roadmap towards a leaner and simpler Passlib 2.0. *Highlights include:* * Support for :class:`~passlib.hash.argon2` and :class:`~passlib.hash.scrypt` hashes. * TOTP Two-Factor Authentications helpers in the :mod:`passlib.totp` module. .. currentmodule:: passlib.ifc * The misnamed :meth:`PasswordHash.encrypt` method has been renamed to :meth:`PasswordHash.hash` (and the old alias deprecated). This is part of a much larger project to clean up passlib's password hashing API, see the :ref:`hash-tutorial` for a walkthrough. * Large speedup of the internal PBKDF2 routines. * Updated documentation Requirements ------------ * **Passlib now requires Python 2.6, 2.7, or >= 3.3**. Support for Python versions 2.5 and 3.0 through 3.2 have been dropped. Support for PyPy 1.x has also been dropped. * The :mod:`passlib.ext.django` extension now requires Django 1.8 or better. Django 1.7 and earlier are no longer supported. New Features ------------ *New Hashes* * :doc:`passlib.hash.argon2 ` -- Support for the Argon2 password hash (:issue:`69`). * :doc:`passlib.hash.scrypt ` -- New password hash format which uses the SCrypt KDF (:issue:`8`). * :doc:`passlib.hash.cisco_asa ` -- Support for Cisco ASA 7.0 and newer hashes (:issue:`51`). *Note: this should be considered experimental, and needs verification of it's test vectors.* *New Modules* * New :mod:`passlib.totp` module provides full support for TOTP tokens on both client and server side. This module contains both low-level primitives, and high-level helpers for persisting and tracking client state. * New :mod:`passlib.pwd` module added to aid in password generation. Features support for alphanumeric passwords, or generation of phrases using the EFF's password generation wordlist. *CryptContext Features* * The :class:`~passlib.context.CryptContext` object now has helper methods for dealing with hashes representing :ref:`disabled accounts ` (:issue:`45`). * All hashers which truncate passwords (e.g. :class:`~passlib.hash.bcrypt` and :class:`~passlib.hash.des_crypt`) can now be configured to raise a :exc:`~passlib.exc.PasswordTruncateError` when a overly-large password is provided. This configurable via (for example) ``bcrypt.using(truncate_error=True).hash(secret)``, or globally as an option to CryptContext (:issue:`59`). *Cryptographic Backends* * The :func:`~passlib.crypto.digest.pbkdf2_hmac` function and all PBKDF2-based hashes have been sped up by ~20% compared to Passlib 1.6. For an even greater speedup, it will now take advantage of the external `fastpbk2 `_ library, or stdlib's :func:`hashlib.pbkdf2_hmac` (when available). Other Changes ------------- *Other changes of note in Passlib 1.7:* .. currentmodule:: passlib.ifc * New workflows have been for configuring the hashers through :meth:`PasswordHash.using`, and testing hashes through :meth:`PasswordHash.needs_update`. See the :ref:`hash-tutorial` for a walkthrough. * :class:`~passlib.hash.bcrypt` and :class:`~passlib.hash.bcrypt_sha256` now default to the "2b" format. * Added support for Django's Argon2 wrapper (:class:`~passlib.hash.django_argon2`) * :class:`passlib.apache.HtpasswdFile` has been updated to support all of Apache 2.4's hash schemes, as well as all host OS crypt formats; allowing for much more secure hashes in htpasswd files. You can now specify if the default hash should be compatible with apache 2.2 or 2.4, and host-specific or portable. See the ``default_schemes`` keyword for details. * Large parts of the documentation have been rewritten, to separate tutorial & api reference content, and provide more detail on various features. * Official documentation is now at https://passlib.readthedocs.io *Internal Changes* .. currentmodule:: passlib.ifc * The majority of CryptContext's internal rounds handling & migration code has been moved to the password hashes themselves, taking advantage of the new :meth:`PasswordHash.using` and :meth:`PasswordHash.needs_update` methods. This allows much more flexibility when configuring a hasher directly, as well making it easier for CryptContext to support hash-specific parameters. * The shared :class:`!PasswordHash` unittests now check all hash handlers for basic thread-safety (motivated by the pybcrypt 0.2 concurrency bug). * :func:`~passlib.utils.consteq` is now wraps stdlib's :func:`hmac.compare_digest` when available (python 2.7.11, python 3.3 and up). Bugfixes -------- * :class:`~passlib.hash.bcrypt`: Passlib will now detect and work around a fatal concurrency bug in py-bcrypt 0.2 and earlier (a :exc:`~passlib.exc.PasslibSecurityWarning` will also be issued). Nevertheless, users are *strongly* encouraged to upgrade to py-bcrypt 0.3 or another bcrypt library if you are using the :doc:`bcrypt ` hash. * :class:`~passlib.CryptContext` instances now pass contextual keywords (such as `"user"`) to the hashes that support them, but ignore them for hashes that don't (:issue:`63`). * The :mod:`passlib.apache` htpasswd helpers now preserve blank lines and comments, rather than throwing a parse error (:issue:`73`). * :mod:`passlib.ext.django` and unittests: compatibility fixes for Django 1.9 / 1.10, and some internal refactoring (:issue:`68`). * The :class:`~passlib.hash.django_disabled` hash now appends a 40-char alphanumeric string, to match Django's behavior. .. _encrypt-method-cleanup: Deprecations ------------ As part of a long-range plan to restructure and simplify both the API and the internals of Passlib, a number of methods have been deprecated & replaced. The eventually goal is a large cleanup and overhaul as part of Passlib 2.0. There will be at least one more 1.x version before Passlib 2.0, to provide a final transitional release (see the `Passlib Roadmap `_). Password Hash API Deprecations .............................. .. currentmodule:: passlib.ifc As part of this cleanup, the :class:`~passlib.ifc.PasswordHash` API (used by all hashes in passlib), has had a number of changes: .. rst-class:: float-right .. seealso:: :ref:`hash-tutorial`, which walks through using the new hasher interface. * **[major]** The :meth:`!PasswordHash.encrypt` method has been renamed to :meth:`PasswordHash.hash`, to clarify that it's performing one-way hashing rather than reversiable encryption. A compatibility alias will remain in place until Passlib 2.0. This should fix the longstanding :issue:`21`. * **[major]** Passing explicit configuration options to the :meth:`!PasswordHash.encrypt` method (now called :meth:`PasswordHash.hash`) is deprecated. To provide settings such as ``rounds`` and ``salt_size``, callers should use the new :meth:`PasswordHash.using` method, which generates a new hasher with a customized configuration. For example, instead of:: >>> sha256_crypt.encrypt("secret", rounds=12345) ... applications should now use:: >>> sha256_crypt.using(rounds=12345).hash("secret") Support for the old syntax will be removed in Passlib 2.0. .. note:: This doesn't apply to contextual options such as :class:`~passlib.hash.cisco_pix`'s ``user`` keyword, which should still be passed into the :meth:`!hash` method. * **[minor]** The little-used :meth:`PasswordHash.genhash` and :meth:`PasswordHash.genconfig` methods have been deprecated. Compatibility aliases will remain in place until Passlib 2.0, at which point they will be removed entirely. Crypt Context API Deprecations .............................. .. currentmodule:: passlib.context Applications which use passlib's :class:`~passlib.context.CryptContext` should not be greatly affected by this release; only one major deprecation was made: * **[major]** To match the :class:`!PasswordHash` API changes above, the :meth:`!CryptContext.encrypt` method was renamed to :meth:`CryptContext.hash`. A compatibility alias will remain until Passlib 2.0. A fewer internal options and infrequently used features have been deprecated: * **[minor]** :meth:`CryptContext.hash`, :meth:`~CryptContext.verify`, :meth:`~CryptContext.verify_and_update`, and :meth:`~CryptContext.needs_update`: The ``scheme`` keyword is now deprecated; support will be removed in Passlib 2.0. * **[minor]** :meth:`CryptContext.hash`: Passing settings keywords to :meth:`!hash` such as ``rounds`` and ``salt`` is deprecated. Code should now get ahold of the default hasher, and invoke it explicitly:: >>> # for example, calls that did this: >>> context.hash(secret, rounds=1234) >>> # should use this instead: >>> context.handler().using(rounds=1234).hash(secret) * **[minor]** The ``vary_rounds`` option has been deprecated, and will be removed in Passlib 2.0. It provided very little security benefit, and was judged not worth the additional code complexity it requires. * **[minor]** The special wildcard ``all`` scheme name has been deprecated, and will be removed in Passlib 2.0. The only legitimate use was to support ``vary_rounds``, which itself will be removed in 2.0. Other Deprecations .................. A few other assorted deprecations have been made: * The :func:`passlib.utils.generate_secret` function has been deprecated in favor of the new :mod:`passlib.pwd` module, and the old function will be removed in Passlib 2.0. * Most of passlib's internal cryptography helpers have been moved from :mod:`passlib.utils` to :mod:`passlib.crypto`, and the APIs refactored. This allowed unification of various hash management routines, some speed ups to the HMAC and PBKDF2 primitives, and opens up the architecture to support more optional backend libraries. Compatibility wrappers will be kept in place at the old location until Passlib 2.0. * Some deprecations and internal changes have been made to the :mod:`passlib.utils.handlers` module, which provides the common framework Passlib uses to implement hashers. .. caution:: More backwards-incompatible relocations are planned for the internal :mod:`!passlib.utils` module in the Passlib 1.8 / 1.9 releases. Backwards Incompatibilities --------------------------- Changes in existing behavior: * **[minor]** M2Crypto no longer used to accelerate pbkdf2-hmac-sha1; applications relying on this to speed up :class:`~passlib.hash.pbkdf2_sha1` should install `fastpbkdf2 `_. Scheduled removal of features: * **[minor]** :mod:`passlib.context`: The :ref:`min_verify_time ` keyword that was deprecated in release 1.6, is now completely ignored. Support will be removed entirely in release 1.8. * **[trivial]** :mod:`passlib.hash`: The internal :meth:`!PasswordHash.parse_rounds` method, deprecated in 1.6, has been removed. Minor incompatibilities: * **[minor]** :mod:`passlib.hash`: The little-used method :meth:`~passlib.ifc.PasswordHash.genconfig` will now always return a valid hash, rather than a truncated configuration string or ``None``. * **[minor]** :mod:`passlib.hash`: The little-used method :meth:`~passlib.ifc.PasswordHash.genhash` no longer accepts ``None`` as a config argument. * **[trivial]** :func:`passlib.utils.pbkdf2.pbkdf2` no longer supports custom PRF callables. this was an unused feature, and prevented some useful optimizations. passlib-1.7.1/docs/overview.rst0000644000175000017500000000014313015205366017632 0ustar biscuitbiscuit00000000000000:orphan: .. redirect stub .. seealso:: This page has been moved to :doc:`narr/overview` passlib-1.7.1/docs/new_app_quickstart.rst0000644000175000017500000000014513015205366021671 0ustar biscuitbiscuit00000000000000:orphan: .. redirect stub .. seealso:: This page has been moved to :doc:`narr/quickstart` passlib-1.7.1/LICENSE0000644000175000017500000001153213043773202015313 0ustar biscuitbiscuit00000000000000.. -*- restructuredtext -*- ===================== Copyrights & Licenses ===================== Credits ======= Passlib is primarily developed by Eli Collins. Special thanks to Darin Gordon for testing and feedback on the :mod:`passlib.totp` module. License for Passlib =================== Passlib is (c) `Assurance Technologies `_, and is released under the `BSD license `_:: Passlib Copyright (c) 2008-2017 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/crypto/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/crypto/_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. Wordsets -------- The EFF wordsets in ``passlib/_data/wordsets`` are (c) 2016 the Electronic Freedom Foundation. They were downloaded from ``_, and are released under the `Creative Commons License `_. passlib-1.7.1/setup.py0000644000175000017500000001431513043774617016035 0ustar biscuitbiscuit00000000000000""" passlib setup script This script honors one environmental variable: SETUP_TAG_RELEASE if "yes" (the default), revision tag is appended to version. for release, this is explicitly set to "no". """ #============================================================================= # 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 setuptools import sys #============================================================================= # init setup options #============================================================================= opts = dict( #================================================================== # sources #================================================================== packages=setuptools.find_packages(root_dir), package_data={ "passlib.tests": ["*.cfg"], "passlib": ["_data/wordsets/*.txt"], }, zip_safe=True, #================================================================== # metadata #================================================================== name="passlib", # NOTE: 'version' set below author="Eli Collins", author_email="elic@assurancetechnologies.com", license="BSD", url="https://bitbucket.org/ecollins/passlib", # NOTE: 'download_url' set below extras_require={ "argon2": "argon2_cffi>=16.2", "bcrypt": "bcrypt>=3.1.0", "totp": "cryptography", }, #================================================================== # details #================================================================== description= "comprehensive password hashing framework supporting over 30 schemes", long_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 `4D8592DF4CE1ED31 `_. """, keywords="""\ password secret hash security crypt md5-crypt sha256-crypt sha512-crypt pbkdf2 argon2 scrypt bcrypt apache htpasswd htdigest totp 2fa """, classifiers="""\ Intended Audience :: Developers License :: OSI Approved :: BSD License Natural Language :: English Operating System :: OS Independent Programming Language :: Python :: 2 Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: Jython Programming Language :: Python :: Implementation :: PyPy Topic :: Security :: Cryptography Topic :: Software Development :: Libraries """.splitlines(), # TODO: add "Programming Language :: Python :: Implementation :: IronPython" # (blocked by issue 34) #================================================================== # testing #================================================================== tests_require='nose >= 1.1', test_suite='nose.collector', #================================================================== # custom setup #================================================================== script_args=sys.argv[1:], cmdclass={}, ) #============================================================================= # set version string #============================================================================= # pull version string from passlib from passlib import __version__ as version # append hg revision to builds stamp_build = False if stamp_build: from passlib._setup.stamp import ( as_bool, append_hg_revision, stamp_distutils_output, install_build_py_exclude, set_command_options ) # add HG revision to end of version if as_bool(os.environ.get("SETUP_TAG_RELEASE", "yes")): version = append_hg_revision(version) # subclass build_py & sdist to rewrite source version string, # and clears stamp_build flag so this doesn't run again. stamp_distutils_output(opts, version) # exclude 'passlib._setup' from builds, only needed for sdist install_build_py_exclude(opts) set_command_options(opts, "build_py", exclude_packages=["passlib._setup"], ) opts['version'] = version #============================================================================= # set release status #============================================================================= if '.dev' in version: status = "Development Status :: 3 - Alpha" elif '.post' in version: status = "Development Status :: 4 - Beta" else: status = "Development Status :: 5 - Production/Stable" # only list download url for final release opts.update( download_url=("https://pypi.python.org/packages/source/p/passlib/" "passlib-" + version + ".tar.gz") ) opts['classifiers'].append(status) #============================================================================= # run setup #============================================================================= setuptools.setup(**opts) #============================================================================= # eof #============================================================================= passlib-1.7.1/README0000644000175000017500000000520313015215267015165 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 `4D8592DF4CE1ED31 `_. * 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.hash("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 - https://passlib.readthedocs.io * Mailing list - https://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 * Roadmap - https://bitbucket.org/ecollins/passlib/wiki/Roadmap 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.7.1/passlib.egg-info/0000755000175000017500000000000013043774617017446 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib.egg-info/PKG-INFO0000644000175000017500000000462713043774617020554 0ustar biscuitbiscuit00000000000000Metadata-Version: 1.1 Name: passlib Version: 1.7.1 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: https://pypi.python.org/packages/source/p/passlib/passlib-1.7.1.tar.gz Description: Passlib is a password hashing library for Python 2 & 3, which provides cross-platform implementations of over 30 password hashing algorithms, as well as a framework for managing existing password hashes. It's designed to be useful for a wide range of tasks, from verifying a hash found in /etc/shadow, to providing full-strength password hashing for multi-user 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 `4D8592DF4CE1ED31 `_. Keywords: password secret hash security crypt md5-crypt sha256-crypt sha512-crypt pbkdf2 argon2 scrypt bcrypt apache htpasswd htdigest totp 2fa 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 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 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.7.1/passlib.egg-info/dependency_links.txt0000644000175000017500000000000113043774617023514 0ustar biscuitbiscuit00000000000000 passlib-1.7.1/passlib.egg-info/requires.txt0000644000175000017500000000011113043774617022037 0ustar biscuitbiscuit00000000000000 [argon2] argon2_cffi>=16.2 [bcrypt] bcrypt>=3.1.0 [totp] cryptography passlib-1.7.1/passlib.egg-info/top_level.txt0000644000175000017500000000001013043774617022167 0ustar biscuitbiscuit00000000000000passlib passlib-1.7.1/passlib.egg-info/SOURCES.txt0000644000175000017500000001431413043774617021335 0ustar biscuitbiscuit00000000000000LICENSE MANIFEST.in README setup.cfg setup.py tox.ini docs/conf.py docs/contents.rst docs/copyright.rst docs/dev-requirements.txt docs/faq.rst docs/history.rst docs/index.rst docs/install.rst docs/modular_crypt_format.rst docs/new_app_quickstart.rst docs/other.rst docs/overview.rst docs/password_hash_api.rst docs/requirements.txt docs/_fragments/asa_verify_callout.rst docs/_fragments/insecure_hash_warning.rst docs/_fragments/trivial_hash_warning.rst docs/_static/bb-logo.png docs/_static/bb-logo.svg 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/history/1.5.rst docs/history/1.6.rst docs/history/1.7.rst docs/history/ancient.rst docs/history/index.rst docs/lib/index.rst docs/lib/passlib.apache.rst docs/lib/passlib.apps.rst docs/lib/passlib.context-tutorial.rst docs/lib/passlib.context.rst docs/lib/passlib.crypto.des.rst docs/lib/passlib.crypto.digest.rst docs/lib/passlib.crypto.rst docs/lib/passlib.exc.rst docs/lib/passlib.ext.django.rst docs/lib/passlib.hash.apr_md5_crypt.rst docs/lib/passlib.hash.argon2.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_asa.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.scrypt.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.ifc.rst docs/lib/passlib.pwd.rst docs/lib/passlib.registry.rst docs/lib/passlib.totp.rst docs/lib/passlib.utils.binary.rst docs/lib/passlib.utils.compat.rst docs/lib/passlib.utils.des.rst docs/lib/passlib.utils.handlers.rst docs/lib/passlib.utils.pbkdf2.rst docs/lib/passlib.utils.rst docs/narr/context-tutorial.rst docs/narr/hash-tutorial.rst docs/narr/index.rst docs/narr/overview.rst docs/narr/quickstart.rst docs/narr/totp-tutorial.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/pwd.py passlib/registry.py passlib/totp.py passlib/win32.py passlib.egg-info/PKG-INFO passlib.egg-info/SOURCES.txt passlib.egg-info/dependency_links.txt passlib.egg-info/requires.txt passlib.egg-info/top_level.txt passlib.egg-info/zip-safe passlib/_data/wordsets/bip39.txt passlib/_data/wordsets/eff_long.txt passlib/_data/wordsets/eff_prefixed.txt passlib/_data/wordsets/eff_short.txt passlib/_setup/__init__.py passlib/_setup/stamp.py passlib/crypto/__init__.py passlib/crypto/_md4.py passlib/crypto/des.py passlib/crypto/digest.py passlib/crypto/_blowfish/__init__.py passlib/crypto/_blowfish/_gen_files.py passlib/crypto/_blowfish/base.py passlib/crypto/_blowfish/unrolled.py passlib/crypto/scrypt/__init__.py passlib/crypto/scrypt/_builtin.py passlib/crypto/scrypt/_gen_files.py passlib/crypto/scrypt/_salsa.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/argon2.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/scrypt.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_crypto_builtin_md4.py passlib/tests/test_crypto_des.py passlib/tests/test_crypto_digest.py passlib/tests/test_crypto_scrypt.py passlib/tests/test_ext_django.py passlib/tests/test_ext_django_source.py passlib/tests/test_handlers.py passlib/tests/test_handlers_argon2.py passlib/tests/test_handlers_bcrypt.py passlib/tests/test_handlers_cisco.py passlib/tests/test_handlers_django.py passlib/tests/test_handlers_pbkdf2.py passlib/tests/test_handlers_scrypt.py passlib/tests/test_hosts.py passlib/tests/test_pwd.py passlib/tests/test_registry.py passlib/tests/test_totp.py passlib/tests/test_utils.py passlib/tests/test_utils_handlers.py passlib/tests/test_utils_md4.py passlib/tests/test_utils_pbkdf2.py passlib/tests/test_win32.py passlib/tests/tox_support.py passlib/tests/utils.py passlib/utils/__init__.py passlib/utils/binary.py passlib/utils/decor.py passlib/utils/des.py passlib/utils/handlers.py passlib/utils/md4.py passlib/utils/pbkdf2.py passlib/utils/compat/__init__.py passlib/utils/compat/_ordered_dict.pypasslib-1.7.1/passlib.egg-info/zip-safe0000644000175000017500000000000112214647077021074 0ustar biscuitbiscuit00000000000000 passlib-1.7.1/PKG-INFO0000644000175000017500000000462713043774617015425 0ustar biscuitbiscuit00000000000000Metadata-Version: 1.1 Name: passlib Version: 1.7.1 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: https://pypi.python.org/packages/source/p/passlib/passlib-1.7.1.tar.gz Description: Passlib is a password hashing library for Python 2 & 3, which provides cross-platform implementations of over 30 password hashing algorithms, as well as a framework for managing existing password hashes. It's designed to be useful for a wide range of tasks, from verifying a hash found in /etc/shadow, to providing full-strength password hashing for multi-user 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 `4D8592DF4CE1ED31 `_. Keywords: password secret hash security crypt md5-crypt sha256-crypt sha512-crypt pbkdf2 argon2 scrypt bcrypt apache htpasswd htdigest totp 2fa 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 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 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.7.1/tox.ini0000664000175000017500000001773213043770632015637 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. # #----------------------------------------------------------------------- # external library integration tests #----------------------------------------------------------------------- # There are a bunch of optional libraries. For the most part, # the convention here is that the 'default' tests check with all the recommended # libraries installed; and then individual envs are added which run a restricted # set of the affected tests, exercising the alternate backends. #=========================================================================== #=========================================================================== # global config #=========================================================================== [tox] minversion=2.3 envlist = # default tests # TODO: would like to 'default-pyston' but doesnt quite work # TODO: also add default-jython27 default-py{26,27,33,34,35,36,py,py3}, # pbkdf2 backend testing # NOTE: 'hashlib' takes priority under py34+ # 'from-bytes' used for py33 (since hashlib not present) # 'unpack' used for py2 ## pdbkf2-fastpbkdf2-py{2,3}, # tested by default config pbkdf2-hashlib-py{3,py3}, pbkdf2-unpack-py{26,27,py}, pbkdf2-frombytes-py{33,py3}, # bcrypt backend testing (bcrypt cffi tested by default test) # NOTE: 'other' checks bcryptor & py-bcrypt ## bcrypthash-bcrypt-py{2,3,py,py3}, # tested by default config bcrypthash-other-py{2,3} bcrypthash-{builtin,disabled}-py{2,3,py,py3} # scrypt backend testing (builtin backend tested by default test) # XXX: 'scrypt' not compatible w/ pypy, or would include this under default. # could still do that for all but pypy, and do special test for builtin. scrypthash-scrypt-py{2,3}, ## scrypthash-builtin-py{2,3,py,py3}, # tested by default config # argon2 backend testing (argon2_cffi tested by default test) ## argon2hash-argon2cffi-py{2,3,py,py3} # tested by default config argon2hash-argon2pure-py{2,3,py,py3}, # django tests # NOTE: django >= 1.7 distributes tests as part of source, not the package, so for full # integration tests to run, caller must provide a copy of the latest django source, # and set the env var PASSLIB_TESTS_DJANGO_SOURCE_PATH to point to it. django18-wdeps-py{2,3}, django-wdeps-py{2,3}, django-nodeps-py{2,3}, # other tests gae-py27 docs #=========================================================================== # common env configuration #=========================================================================== [testenv] basepython = py2: python2 py26: python2.6 py27: python2.7 py3: python3 py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 pypy: pypy pypy3: pypy3 jython27: jython2.7 passenv = PASSLIB_TEST_MODE PASSLIB_TESTS_DJANGO_SOURCE_PATH NOSE_REDNOSE NOSE_REDNOSE_COLOR setenv = # test mode setup PASSLIB_TEST_MODE = {env:PASSLIB_TEST_MODE:full} bcrypthash-builtin: PASSLIB_BUILTIN_BCRYPT = enabled bcrypthash-disabled: PASSLIB_TEST_MODE = quick # nose option fragments with_coverage: TEST_COVER_OPTS = --with-xunit --with-coverage --cover-xml --cover-package passlib TEST_OPTS = --hide-skips --randomize {env:TEST_COVER_OPTS:} changedir = {envdir} commands = # default tests default: nosetests {posargs:{env:TEST_OPTS} passlib.tests} # crypto backend tests pbkdf2: nosetests {posargs:{env:TEST_OPTS} passlib.tests.test_crypto_digest passlib.tests.test_handlers_pbkdf2} # hash backend tests bcrypthash: nosetests {posargs:{env:TEST_OPTS} passlib.tests.test_handlers_bcrypt} scrypthash: nosetests {posargs:{env:TEST_OPTS} passlib.tests.test_crypto_scrypt passlib.tests.test_handlers_scrypt} argon2hash: nosetests {posargs:{env:TEST_OPTS} passlib.tests.test_handlers_argon2} # django tests django{,18}: nosetests {posargs:{env:TEST_OPTS} passlib.tests.test_ext_django passlib.tests.test_handlers_django} deps = # common nose rednose coverage randomize unittest2 # totp helper tests # NOTE: cryptography requires python-dev, libffi-dev, libssl-dev # XXX: 2016-6-20: having issue w/ cryptography under pypy, disabling it for now default-py{2,26,27,3,33,34,35,36}: cryptography # pbkdf2 backend tests # NOTE: fastpbkdf2 requires python-dev, libffi-dev, libssl-dev default,pbkdf2-fastpbkdf2: fastpbkdf2 # pbkdf2-{hashlib,unpack,from_bytes} -- no deps # bcrypt backend tests # NOTE: bcrypt requires python-dev, libffi-dev # NOTE: bcryptor is py2 only, requires python-dev & Cython # NOTE: bcrypt10 env disabled, just used to check legacy issues ## bcrypthash-bcrypt10: bcrypt<1.1 default,bcrypthash-bcrypt: bcrypt bcrypthash-other-py{2,26,27}: bcryptor bcrypthash-other: py-bcrypt # scrypt backend tests # XXX: would test 'scrypt' under default, but not compatible w/ pypy, # so using default test to check builtin backend; # could just omit it from pypy tests instead. scrypthash-scrypt: scrypt # scrypthash-builtin -- nodeps # argon2 backend tests # NOTE: argon2_cffi requires python-dev, libffi-dev default,argon2hash-argon2cffi: argon2_cffi argon2hash-argon2pure: argon2pure # django extension tests django18: django>=1.8,<1.9 django: django django{,18}-deps: bcrypt # django{,18}-nodeps -- would like to use this as negative dependancy for 'bcrypt' instead # needed by django's internal tests django{,18}: mock #=========================================================================== # 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 rednose 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} #=========================================================================== # build documentation #=========================================================================== [testenv:docs] basepython = python changedir = docs deps = sphinx commands = pip install -r requirements.txt sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html python -c 'print("HTML_DEST_DIR={envtmpdir}/html")' #=========================================================================== # eof #=========================================================================== passlib-1.7.1/MANIFEST.in0000644000175000017500000000021513015205366016040 0ustar biscuitbiscuit00000000000000recursive-include docs * recursive-include passlib/_data/wordsets *.txt include LICENSE README CHANGES passlib/tests/*.cfg tox.ini setup.cfg passlib-1.7.1/passlib/0000755000175000017500000000000013043774617015754 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/__init__.py0000644000175000017500000000012713043774617020065 0ustar biscuitbiscuit00000000000000"""passlib - suite of password hashing & generation routines""" __version__ = '1.7.1' passlib-1.7.1/passlib/ifc.py0000644000175000017500000003356013043701620017056 0ustar biscuitbiscuit00000000000000"""passlib.ifc - abstract interfaces used by Passlib""" #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) import sys # site # pkg from passlib.utils.decor import deprecated_method # local __all__ = [ "PasswordHash", ] #============================================================================= # 2/3 compatibility helpers #============================================================================= def recreate_with_metaclass(meta): """class decorator that re-creates class using metaclass""" def builder(cls): if meta is type(cls): return cls return meta(cls.__name__, cls.__bases__, cls.__dict__.copy()) return builder #============================================================================= # PasswordHash interface #============================================================================= from abc import ABCMeta, abstractmethod, abstractproperty # TODO: make this actually use abstractproperty(), # now that we dropped py25, 'abc' is always available. # XXX: rename to PasswordHasher? @recreate_with_metaclass(ABCMeta) 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 #: flag which indicates this hasher matches a "disabled" hash #: (e.g. unix_disabled, or django_disabled); and doesn't actually #: depend on the provided password. is_disabled = False #: Should be None, or a positive integer indicating hash #: doesn't support secrets larger than this value. #: Whether hash throws error or silently truncates secret #: depends on .truncate_error and .truncate_verify_reject flags below. #: NOTE: calls may treat as boolean, since value will never be 0. #: .. versionadded:: 1.7 #: .. TODO: passlib 1.8: deprecate/rename this attr to "max_secret_size"? truncate_size = None # NOTE: these next two default to the optimistic "ideal", # most hashes in passlib have to default to False # for backward compat and/or expected behavior with existing hashes. #: If True, .hash() should throw a :exc:`~passlib.exc.PasswordSizeError` for #: any secrets larger than .truncate_size. Many hashers default to False #: for historical / compatibility purposes, indicating they will silently #: truncate instead. All such hashers SHOULD support changing #: the policy via ``.using(truncate_error=True)``. #: .. versionadded:: 1.7 #: .. TODO: passlib 1.8: deprecate/rename this attr to "truncate_hash_error"? truncate_error = True #: If True, .verify() should reject secrets larger than max_password_size. #: Many hashers default to False for historical / compatibility purposes, #: indicating they will match on the truncated portion instead. #: .. versionadded:: 1.7.1 truncate_verify_reject = True #--------------------------------------------------------------- # 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 hash(cls, secret, # * **setting_and_context_kwds): # pragma: no cover -- abstract method r""" Hash secret, returning result. Should handle generating salt, etc, and should return string containing identifier, salt & other configuration, as well as digest. :param \*\*settings_kwds: Pass in settings to customize configuration of resulting hash. .. deprecated:: 1.7 Starting with Passlib 1.7, callers should no longer pass settings keywords (e.g. ``rounds`` or ``salt`` directly to :meth:`!hash`); should use ``.using(**settings).hash(secret)`` construction instead. Support will be removed in Passlib 2.0. :param \*\*context_kwds: Specific algorithms may require context-specific information (such as the user login). """ # FIXME: need stub for classes that define .encrypt() instead ... # this should call .encrypt(), and check for recursion back to here. raise NotImplementedError("must be implemented by subclass") @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") @classmethod def encrypt(cls, *args, **kwds): """ Legacy alias for :meth:`hash`. .. deprecated:: 1.7 This method was renamed to :meth:`!hash` in version 1.7. This alias will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.6. """ return cls.hash(*args, **kwds) # XXX: could provide default implementation which hands value to # hash(), and then does constant-time comparision on the result # (after making both are same string type) @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") #=================================================================== # configuration #=================================================================== @classmethod @abstractmethod def using(cls, relaxed=False, **kwds): """ Return another hasher object (typically a subclass of the current one), which integrates the configuration options specified by ``kwds``. This should *always* return a new object, even if no configuration options are changed. .. todo:: document which options are accepted. :returns: typically returns a subclass for most hasher implementations. .. todo:: add this method to main documentation. """ raise NotImplementedError("must be implemented by subclass") #=================================================================== # migration #=================================================================== @classmethod def needs_update(cls, hash, secret=None): """ check if hash's configuration is outside desired bounds, or contains some other internal option which requires updating the password hash. :param hash: hash string to examine :param secret: optional secret known to have verified against the provided hash. (this is used by some hashes to detect legacy algorithm mistakes). :return: whether secret needs re-hashing. .. versionadded:: 1.7 """ # by default, always report that we don't need update return False #=================================================================== # 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") @deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls, **setting_kwds): # pragma: no cover -- abstract method """ compile settings into a configuration string for genhash() .. deprecated:: 1.7 As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. For all known real-world uses, hashing a constant string should provide equivalent functionality. This deprecation may be reversed if a use-case presents itself in the mean time. """ # NOTE: this fallback runs full hash alg, w/ whatever cost param is passed along. # implementations (esp ones w/ variable cost) will want to subclass this # with a constant-time implementation that just renders a config string. if cls.context_kwds: raise NotImplementedError("must be implemented by subclass") return cls.using(**setting_kwds).hash("") @deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config, **context): """ generated hash for secret, using settings from config/hash string .. deprecated:: 1.7 As of 1.7, this method is deprecated, and slated for complete removal in Passlib 2.0. This deprecation may be reversed if a use-case presents itself in the mean time. """ # XXX: if hashes reliably offered a .parse() method, could make a fallback for this. 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*. #--------------------------------------------------------------- # extra metdata #--------------------------------------------------------------- #: this attribute shouldn't be used by hashers themselves, #: it's reserved for the CryptContext to track which hashers are deprecated. #: Note the context will only set this on objects it owns (and generated by .using()), #: and WONT set it on global objects. #: [added in 1.7] #: TODO: document this, or at least the use of testing for #: 'CryptContext().handler().deprecated' deprecated = False #: optionally present if hasher corresponds to format built into Django. #: this attribute (if not None) should be the Django 'algorithm' name. #: also indicates to passlib.ext.django that (when installed in django), #: django's native hasher should be used in preference to this one. ## django_name #--------------------------------------------------------------- # checksum information - defined for many hashes #--------------------------------------------------------------- ## checksum_chars ## checksum_size #--------------------------------------------------------------- # 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 #=================================================================== class DisabledHash(PasswordHash): """ extended disabled-hash methods; only need be present if .disabled = True """ is_disabled = True @classmethod def disable(cls, hash=None): """ return string representing a 'disabled' hash; optionally including previously enabled hash (this is up to the individual scheme). """ # default behavior: ignore original hash, return standalone marker return cls.hash("") @classmethod def enable(cls, hash): """ given a disabled-hash string, extract previously-enabled hash if one is present, otherwise raises ValueError """ # default behavior: no way to restore original hash raise ValueError("cannot restore original hash") #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/context.py0000644000175000017500000032475613043457152020023 0ustar biscuitbiscuit00000000000000"""passlib.context - CryptContext implementation""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import re import logging; log = logging.getLogger(__name__) import threading import time from warnings import warn # site # pkg from passlib.exc import ExpectedStringError, ExpectedTypeError, PasslibConfigWarning from passlib.registry import get_crypt_handler, _validate_handler_name from passlib.utils import (handlers as uh, to_bytes, to_unicode, splitcomma, as_bool, timer, rng, getrandstr, ) from passlib.utils.binary import BASE64_CHARS from passlib.utils.compat import (iteritems, num_types, irange, PY2, PY3, unicode, SafeConfigParser, NativeStringIO, BytesIO, unicode_or_bytes_types, native_string_types, ) from passlib.utils.decor import deprecated_method, memoized_property # local __all__ = [ 'CryptContext', 'LazyCryptContext', 'CryptPolicy', ] #============================================================================= # support #============================================================================= # private object to detect unset params _UNSET = object() 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. # NOTE: this isn't really needed any longer, since Handler.using() handles the actual parsing. # keeping this around for now, though, since it makes context.to_dict() output cleaner. _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 @staticmethod def _always_needs_update(hash, secret=None): """ dummy function patched into handler.needs_update() by _CryptConfig when hash alg has been deprecated for context. """ return True #: list of keys allowed under wildcard "all" scheme w/o a security warning. _global_settings = set(["truncate_error", "vary_rounds"]) #============================================================================= # 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, unconfigured=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, unconfigured=True)) 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, unconfigured=True) 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 option will be removed entirely in passlib 1.8 .. versionchanged:: 1.7 this method now always returns the value automatically calculated by :meth:`CryptContext.min_verify_time`, any value specified by policy is ignored. """ warn("get_min_verify_time() and min_verify_time option is deprecated and ignored, " "and will be removed in Passlib 1.8", DeprecationWarning, stacklevel=2) return 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.handler(name, category).deprecated #=================================================================== # 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 #=================================================================== #============================================================================= # _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 # set of all context keywords used by active schemes context_kwds = None # dict mapping category -> default scheme _default_schemes = None # dict mapping (scheme, category) -> custom handler _records = None # dict mapping category -> list of custom handler 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) explicit_scheme = scheme if not cat and not scheme and key in _global_settings: # going forward, not using "__all__" format. instead... # whitelisting set of keys which should be passed to (all) schemes, # rather than passed to the CryptContext itself scheme = "all" if scheme: # normalize scheme option key, value = norm_scheme_option(key, value) # e.g. things like "min_rounds" should never be set cross-scheme # this will be fatal under 2.0. if scheme == "all" and key not in _global_settings: warn("The '%s' option should be configured per-algorithm, and not set " "globally in the context; This will be an error in Passlib 2.0" % (key,), PasslibConfigWarning) # this scheme is going away in 2.0; # but most keys deserve an extra warning since it impacts security. if explicit_scheme == "all": warn("The 'all' scheme is deprecated as of Passlib 1.7, " "and will be removed in Passlib 2.0; Please configure " "options on a per-algorithm basis.", DeprecationWarning) # 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(cat, key, value) if key == "min_verify_time": # ignored in 1.7, to be removed in 1.8 continue # 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 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, cat, 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: # XXX: have any statements been made about when this is default? # should do it in 1.8 at latest. 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' was deprecated in Passlib 1.6, is " "ignored in 1.7, and will be removed in 1.8", DeprecationWarning) elif key == "harden_verify": warn("'harden_verify' is deprecated & ignored as of Passlib 1.7.1, " " and will be removed in 1.8", DeprecationWarning) 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_base_handler(self, scheme): return self.handlers[self.schemes.index(scheme)] @staticmethod def expand_settings(handler): setting_kwds = handler.setting_kwds if 'rounds' in handler.setting_kwds: # XXX: historically this extras won't be listed in setting_kwds setting_kwds += uh.HasRounds.using_rounds_kwds return setting_kwds # NOTE: this is only used by _get_record_options_with_flag()... 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)) # filter out global settings not supported by handler allowed_settings = self.expand_settings(self.get_base_handler(scheme)) for key in set(kwds).difference(allowed_settings): kwds.pop(key) if category: for key in set(defkwds).difference(allowed_settings): defkwds.pop(key) # 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 violations 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 = {} all_context_kwds = self.context_kwds = set() get_options = self._get_record_options_with_flag categories = (None,) + self.categories for handler in self.handlers: scheme = handler.name all_context_kwds.update(handler.context_kwds) for cat in categories: kwds, has_cat_options = get_options(scheme, cat) if cat is None or has_cat_options: records[scheme, cat] = self._create_record(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(). @staticmethod def _create_record(handler, category=None, deprecated=False, **settings): # create custom handler if needed. try: # XXX: relaxed=True is mostly here to retain backwards-compat behavior. # could make this optional flag in future. subcls = handler.using(relaxed=True, **settings) except TypeError as err: m = re.match(r".* unexpected keyword argument '(.*)'$", str(err)) if m and m.group(1) in settings: # translate into KeyError, for backwards compat. # XXX: push this down to GenericHandler.using() implementation? key = m.group(1) raise KeyError("keyword not supported by %s handler: %r" % (handler.name, key)) raise # using private attrs to store some extra metadata in custom handler assert subcls is not handler, "expected unique variant of handler" ##subcls._Context__category = category subcls._Context__orig_handler = handler subcls.deprecated = deprecated # attr reserved for this purpose return subcls 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 :meth:`_create_record`, 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* keyword. """ # 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 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 custom handler 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, unicode_or_bytes_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") @memoized_property def disabled_record(self): for record in self._get_record_list(None): if record.is_disabled: return record raise RuntimeError("no disabled hasher present " "(perhaps add 'unix_disabled' to list of schemes?)") #=================================================================== # 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 hashing & verifying passwords using multiple 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. # XXX: add wrap/unwrap callback hooks so app can mutate hash format? # XXX: add method for detecting and warning user about schemes # which don't have any good distinguishing marks? # or greedy ones (unix_disabled, plaintext) which are not listed at the end? #=================================================================== # 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, unicode_or_bytes_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 using(self, **kwds): """ alias for :meth:`copy`, to match PasswordHash.using() """ return self.copy(**kwds) 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 PY3: # python 3.2 deprecated readfp in favor of read_file p.read_file(stream, filename) else: p.readfp(stream, filename) # XXX: could change load() to accept list of items, # and skip intermediate dict creation 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, unicode_or_bytes_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, # and 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._reset_dummy_verify() self._get_record = config.get_record self._identify_record = config.identify_record if config.context_kwds: # (re-)enable method for this instance (in case ELSE clause below ran last load). self.__dict__.pop("_strip_unused_context_kwds", None) else: # disable method for this instance, it's not needed. self._strip_unused_context_kwds = None @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, category=None, unconfigured=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. """ # XXX: should resolv return records rather than handlers? # or deprecate resolve keyword completely? # offering up a .hashers Mapping in v1.8 would be great. # NOTE: supporting 'category' and 'unconfigured' kwds as of 1.7 # just to pass through to .handler(), but not documenting them... # may not need to put them to use. schemes = self._config.schemes if resolve: return tuple(self.handler(scheme, category, unconfigured=unconfigured) for scheme in schemes) else: return schemes def default_scheme(self, category=None, resolve=False, unconfigured=False): """return name of scheme that :meth:`hash` 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 .. versionchanged:: 1.7 This now returns a hasher configured with any CryptContext-specific options (custom rounds settings, etc). Previously this returned the base hasher from :mod:`passlib.hash`. """ # XXX: deprecate this in favor of .handler() or whatever it's replaced with? # NOTE: supporting 'unconfigured' kwds as of 1.7 # just to pass through to .handler(), but not documenting them... # may not need to put them to use. hasher = self.handler(None, category, unconfigured=unconfigured) return hasher if resolve else hasher.name # 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 # XXX: need to decide if exposing this would be useful to applications # in any meaningful way that isn't already served by to_dict() ##def options(self, scheme, category=None): ## kwds, percat = self._config.get_options(scheme, category) ## return kwds def handler(self, scheme=None, category=None, unconfigured=False): """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. :param unconfigured: By default, this returns a handler object whose .hash() and .needs_update() methods will honor the configured provided by CryptContext. See ``unconfigured=True`` to get the underlying handler from before any context-specific configuration was applied. :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()`` .. versionchanged:: 1.7 This now returns a hasher configured with any CryptContext-specific options (custom rounds settings, etc). Previously this returned the base hasher from :mod:`passlib.hash`. """ try: hasher = self._get_record(scheme, category) if unconfigured: return hasher._Context__orig_handler else: return hasher 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)) @property def context_kwds(self): """ return :class:`!set` containing union of all :ref:`contextual keywords ` supported by the handlers in this context. .. versionadded:: 1.6.6 """ return self._config.context_kwds #=================================================================== # 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() #=================================================================== # verify() hardening # NOTE: this entire feature has been disabled. # all contents of this section are NOOPs as of 1.7.1, # and will be removed in 1.8. #=================================================================== mvt_estimate_max_samples = 20 mvt_estimate_min_samples = 10 mvt_estimate_max_time = 2 mvt_estimate_resolution = 0.01 harden_verify = None min_verify_time = 0 def reset_min_verify_time(self): self._reset_dummy_verify() #=================================================================== # password hash api #=================================================================== # NOTE: all the following methods do is look up the appropriate # custom handler for a given (scheme,category) combination, # and hand off the real work to the handler itself, # which is optimized for the specific (scheme,category) configuration. # # The custom handlers 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, unicode_or_bytes_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 _strip_unused_context_kwds(self, kwds, record): """ helper which removes any context keywords from **kwds** that are known to be used by another scheme in this context, but are NOT supported by handler specified by **record**. .. note:: as optimization, load() will set this method to None on a per-instance basis if there are no context kwds. """ if not kwds: return unused_kwds = self._config.context_kwds.difference(record.context_kwds) for key in unused_kwds: kwds.pop(key, None) 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-hashed using :meth:`hash` 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*. .. deprecated:: 1.7 Support for this keyword is deprecated, and will be removed in Passlib 2.0. :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. """ if scheme is not None: # TODO: offer replacement alternative. # ``context.handler(scheme).needs_update()`` would work, # but may deprecate .handler() in passlib 1.8. warn("CryptContext.needs_update(): 'scheme' keyword is deprecated as of " "Passlib 1.7, and will be removed in Passlib 2.0", DeprecationWarning) record = self._get_or_identify_record(hash, scheme, category) return record.deprecated or record.needs_update(hash, secret=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) @deprecated_method(deprecated="1.7", removed="2.0") def genconfig(self, scheme=None, category=None, **settings): """Generate a config string for specified scheme. .. deprecated:: 1.7 This method will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.6. """ record = self._get_record(scheme, category) strip_unused = self._strip_unused_context_kwds if strip_unused: strip_unused(settings, record) return record.genconfig(**settings) @deprecated_method(deprecated="1.7", removed="2.0") def genhash(self, secret, config, scheme=None, category=None, **kwds): """Generate hash for the specified secret using another hash. .. deprecated:: 1.7 This method will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.6. """ record = self._get_or_identify_record(config, scheme, category) strip_unused = self._strip_unused_context_kwds if strip_unused: strip_unused(kwds, record) return record.genhash(secret, config, **kwds) def identify(self, hash, category=None, resolve=False, required=False, unconfigured=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: if unconfigured: return record._Context__orig_handler else: return record else: return record.name def hash(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. .. deprecated:: 1.7 Support for this keyword is deprecated, and will be removed in Passlib 2.0. :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.hash() ` 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.hash() ` method. .. seealso:: the :ref:`context-basic-example` example in the tutorial """ # XXX: could insert normalization to preferred unicode encoding here if scheme is not None: # TODO: offer replacement alternative. # ``context.handler(scheme).hash()`` would work, # but may deprecate .handler() in passlib 1.8. warn("CryptContext.hash(): 'scheme' keyword is deprecated as of " "Passlib 1.7, and will be removed in Passlib 2.0", DeprecationWarning) record = self._get_record(scheme, category) strip_unused = self._strip_unused_context_kwds if strip_unused: strip_unused(kwds, record) return record.hash(secret, **kwds) @deprecated_method(deprecated="1.7", removed="2.0", replacement="CryptContext.hash()") def encrypt(self, *args, **kwds): """ Legacy alias for :meth:`hash`. .. deprecated:: 1.7 This method was renamed to :meth:`!hash` in version 1.7. This alias will be removed in version 2.0, and should only be used for compatibility with Passlib 1.3 - 1.6. """ return self.hash(*args, **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 hash: unicode or bytes :arg hash: hash string to compare to if ``None`` is passed in, this will be treated as "never verifying" :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). .. deprecated:: 1.7 Support for this keyword is deprecated, and will be removed in Passlib 2.0. :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: could insert normalization to preferred unicode encoding here # XXX: what about supporting a setter() callback ala django 1.4 ? if scheme is not None: # TODO: offer replacement alternative. # ``context.handler(scheme).verify()`` would work, # but may deprecate .handler() in passlib 1.8. warn("CryptContext.verify(): 'scheme' keyword is deprecated as of " "Passlib 1.7, and will be removed in Passlib 2.0", DeprecationWarning) if hash is None: # convenience feature -- let apps pass in hash=None when user # isn't found / has no hash; useful because it invokes dummy_verify() self.dummy_verify() return False record = self._get_or_identify_record(hash, scheme, category) strip_unused = self._strip_unused_context_kwds if strip_unused: strip_unused(kwds, record) 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.hash`), 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. if ``None`` is passed in, this will be treated as "never verifying" :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). .. deprecated:: 1.7 Support for this keyword is deprecated, and will be removed in Passlib 2.0. :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: could insert normalization to preferred unicode encoding here. if scheme is not None: warn("CryptContext.verify(): 'scheme' keyword is deprecated as of " "Passlib 1.7, and will be removed in Passlib 2.0", DeprecationWarning) if hash is None: # convenience feature -- let apps pass in hash=None when user # isn't found / has no hash; useful because it invokes dummy_verify() self.dummy_verify() return False, None record = self._get_or_identify_record(hash, scheme, category) strip_unused = self._strip_unused_context_kwds if strip_unused and kwds: clean_kwds = kwds.copy() strip_unused(clean_kwds, record) else: clean_kwds = kwds # XXX: if record is default scheme, could extend PasswordHash # api to combine verify & needs_update to single call, # potentially saving some round-trip parsing. # but might make these codepaths more complex... if not record.verify(secret, hash, **clean_kwds): return False, None elif record.deprecated or record.needs_update(hash, secret=secret): # NOTE: we re-hash with default scheme, not current one. return True, self.hash(secret, category=category, **kwds) else: return True, None #=================================================================== # missing-user helper #=================================================================== #: secret used for dummy_verify() _dummy_secret = "too many secrets" @memoized_property def _dummy_hash(self): """ precalculated hash for dummy_verify() to use """ return self.hash(self._dummy_secret) def _reset_dummy_verify(self): """ flush memoized values used by dummy_verify() """ type(self)._dummy_hash.clear_cache(self) def dummy_verify(self, elapsed=0): """ Helper that applications can call when user wasn't found, in order to simulate time it would take to hash a password. Runs verify() against a dummy hash, to simulate verification of a real account password. :param elapsed: .. deprecated:: 1.7.1 this option is ignored, and will be removed in passlib 1.8. .. versionadded:: 1.7 """ self.verify(self._dummy_secret, self._dummy_hash) return False #=================================================================== # disabled hash support #=================================================================== def is_enabled(self, hash): """ test if hash represents a usuable password -- i.e. does not represent an unusuable password such as ``"!"``, which is recognized by the :class:`~passlib.hash.unix_disabled` hash. :raises ValueError: if the hash is not recognized (typically solved by adding ``unix_disabled`` to the list of schemes). """ return not self._identify_record(hash, None).is_disabled def disable(self, hash=None): """ return a string to disable logins for user, usually by returning a non-verifying string such as ``"!"``. :param hash: Callers can optionally provide the account's existing hash. Some disabled handlers (such as :class:`!unix_disabled`) will encode this into the returned value, so that it can be recovered via :meth:`enable`. :raises RuntimeError: if this function is called w/o a disabled hasher (such as :class:`~passlib.hash.unix_disabled`) included in the list of schemes. :returns: hash string which will be recognized as valid by the context, but is guaranteed to not validate against *any* password. """ record = self._config.disabled_record assert record.is_disabled return record.disable(hash) def enable(self, hash): """ inverse of :meth:`disable` -- attempts to recover original hash which was converted by a :meth:`!disable` call into a disabled hash -- thus restoring the user's original password. :raises ValueError: if original hash not present, or if the disabled handler doesn't support encoding the original hash (e.g. ``django_disabled``) :returns: the original hash. """ record = self._identify_record(hash, None) if record.is_disabled: # XXX: should we throw error if result can't be identified by context? return record.enable(hash) else: # hash wasn't a disabled hash, so return unchanged return hash #=================================================================== # 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.7.1/passlib/utils/0000755000175000017500000000000013043774617017114 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/utils/md4.py0000644000175000017500000000230213015205366020134 0ustar biscuitbiscuit00000000000000""" passlib.utils.md4 - DEPRECATED MODULE, WILL BE REMOVED IN 2.0 MD4 should now be looked up through ``passlib.crypto.digest.lookup_hash("md4").const``, which provides unified handling stdlib implementation (if present). """ #============================================================================= # issue deprecation warning for module #============================================================================= from warnings import warn warn("the module 'passlib.utils.md4' is deprecated as of Passlib 1.7, " "and will be removed in Passlib 2.0, please use " "'lookup_hash(\"md4\").const()' from 'passlib.crypto' instead", DeprecationWarning) #============================================================================= # backwards compat exports #============================================================================= __all__ = ["md4"] # this should use hashlib version if available, # and fall back to builtin version. from passlib.crypto.digest import lookup_hash md4 = lookup_hash("md4").const del lookup_hash #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/utils/__init__.py0000644000175000017500000011000113016616270021204 0ustar biscuitbiscuit00000000000000"""passlib.utils -- helpers for writing password hashes""" #============================================================================= # imports #============================================================================= from passlib.utils.compat import JYTHON # core from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError from base64 import b64encode, b64decode import collections from codecs import lookup as _lookup_codec from functools import update_wrapper import itertools import inspect import logging; log = logging.getLogger(__name__) import math import os import sys import random import re 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 import types from warnings import warn # site # pkg from passlib.utils.binary import ( # [remove these aliases in 2.0] BASE64_CHARS, AB64_CHARS, HASH64_CHARS, BCRYPT_CHARS, Base64Engine, LazyBase64Engine, h64, h64big, bcrypt64, ab64_encode, ab64_decode, b64s_encode, b64s_decode ) from passlib.utils.decor import ( # [remove these aliases in 2.0] deprecated_function, deprecated_method, memoized_property, classproperty, hybrid_method, ) from passlib.exc import ExpectedStringError from passlib.utils.compat import (add_doc, join_bytes, join_byte_values, join_byte_elems, irange, imap, PY3, u, join_unicode, unicode, byte_elem_value, nextgetter, unicode_or_bytes_types, get_method_function, suppress_cause) # local __all__ = [ # constants 'JYTHON', 'sys_bits', 'unix_crypt_schemes', 'rounds_cost_values', # 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', # 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. # XXX: move to .registry for passlib 2.0? 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) #============================================================================= # type helpers #============================================================================= class SequenceMixin(object): """ helper which lets result object act like a fixed-length sequence. subclass just needs to provide :meth:`_as_tuple()`. """ def _as_tuple(self): raise NotImplemented("implement in subclass") def __repr__(self): return repr(self._as_tuple()) def __getitem__(self, idx): return self._as_tuple()[idx] def __iter__(self): return iter(self._as_tuple()) def __len__(self): return len(self._as_tuple()) def __eq__(self, other): return self._as_tuple() == other def __ne__(self, other): return not self.__eq__(other) if PY3: # getargspec() is deprecated, use this under py3. # even though it's a lot more awkward to get basic info :| _VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD _VAR_ANY_SET = set([_VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL]) def accepts_keyword(func, key): """test if function accepts specified keyword""" params = inspect.signature(get_method_function(func)).parameters if not params: return False arg = params.get(key) if arg and arg.kind not in _VAR_ANY_SET: return True # XXX: annoying what we have to do to determine if VAR_KWDS in use. return params[list(params)[-1]].kind == _VAR_KEYWORD else: def accepts_keyword(func, key): """test if function accepts specified keyword""" spec = inspect.getargspec(get_method_function(func)) return key in spec.args or spec.keywords is not None def update_mixin_classes(target, add=None, remove=None, append=False, before=None, after=None, dryrun=False): """ helper to update mixin classes installed in target class. :param target: target class whose bases will be modified. :param add: class / classes to install into target's base class list. :param remove: class / classes to remove from target's base class list. :param append: by default, prepends mixins to front of list. if True, appends to end of list instead. :param after: optionally make sure all mixins are inserted after this class / classes. :param before: optionally make sure all mixins are inserted before this class / classes. :param dryrun: optionally perform all calculations / raise errors, but don't actually modify the class. """ if isinstance(add, type): add = [add] bases = list(target.__bases__) # strip out requested mixins if remove: if isinstance(remove, type): remove = [remove] for mixin in remove: if add and mixin in add: continue if mixin in bases: bases.remove(mixin) # add requested mixins if add: for mixin in add: # if mixin already present (explicitly or not), leave alone if any(issubclass(base, mixin) for base in bases): continue # determine insertion point if append: for idx, base in enumerate(bases): if issubclass(mixin, base): # don't insert mixin after one of it's own bases break if before and issubclass(base, before): # don't insert mixin after any classes. break else: # append to end idx = len(bases) elif after: for end_idx, base in enumerate(reversed(bases)): if issubclass(base, after): # don't insert mixin before any classes. idx = len(bases) - end_idx assert bases[idx-1] == base break else: idx = 0 else: # insert at start idx = 0 # insert mixin bases.insert(idx, mixin) # modify class if not dryrun: target.__bases__ = tuple(bases) #============================================================================= # collection helpers #============================================================================= def batch(source, size): """ split iterable into chunks of elements. """ if size < 1: raise ValueError("size must be positive integer") if isinstance(source, collections.Sequence): end = len(source) i = 0 while i < end: n = i + size yield source[i:n] i = n elif isinstance(source, collections.Iterable): itr = iter(source) while True: chunk_itr = itertools.islice(itr, size) try: first = next(chunk_itr) except StopIteration: break yield itertools.chain((first,), chunk_itr) else: raise TypeError("source must be iterable") #============================================================================= # unicode helpers #============================================================================= # XXX: should this be moved to passlib.crypto, or compat backports? def consteq(left, right): """Check two strings/bytes for equality. This function uses an approach designed to prevent timing analysis, making it appropriate for cryptography. a and b must both be of the same type: either str (ASCII only), or any type that supports the buffer protocol (e.g. bytes). Note: If a and b are of different lengths, or if an error occurs, a timing attack could theoretically reveal information about the types and lengths of a and b--but not their values. """ # 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 # keep copy of this around since stdlib's version throws error on non-ascii chars in unicode strings. # our version does, but suffers from some underlying VM issues. but something is better than # nothing for plaintext hashes, which need this. everything else should use consteq(), # since the stdlib one is going to be as good / better in the general case. str_consteq = consteq try: # for py3.3 and up, use the stdlib version from hmac import compare_digest as consteq except ImportError: pass # TODO: could check for cryptography package's version, # but only operates on bytes, so would need a wrapper, # or separate consteq() into a unicode & a bytes variant. # from cryptography.hazmat.primitives.constant_time import bytes_eq as consteq 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 identifying source parameter in error messages (Defaults to the string ``"value"``). This is mainly useful to make the caller's error messages make more sense contextually. :raises ValueError: if any characters forbidden by the SASLPrep profile are encountered. :raises TypeError: if input is not :class:`!unicode` :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 # XXX: support bytes (e.g. run through want_unicode)? # might be easier to just integrate this into cryptcontext. 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 PY3: # new in py32 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) 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") _true_set = set("true t yes y on 1 enable enabled".split()) _false_set = set("false f no n off 0 disable disabled".split()) _none_set = set(["", "none"]) def as_bool(value, none=None, param="boolean"): """ helper to convert value to boolean. recognizes strings such as "true", "false" """ assert none in [True, False, None] if isinstance(value, unicode_or_bytes_types): clean = value.lower().strip() if clean in _true_set: return True if clean in _false_set: return False if clean in _none_set: return none raise ValueError("unrecognized %s value: %r" % (param, value)) elif isinstance(value, bool): return value elif value is None: return none else: return bool(value) #============================================================================= # 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 timer else: # On most other platforms the best timer is time.time() from time import time as timer # legacy alias, will be removed in passlib 2.0 tick = timer def parse_version(source): """helper to parse version string""" m = re.search(r"(\d+(?:\.\d+)+)", source) if m: return tuple(int(elem) for elem in m.group(1).split(".")) return None #============================================================================= # 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 if hasattr(value, "getstate") and hasattr(value, "getrandbits"): # caller passed in RNG as seed value try: value = value.getstate() except NotImplementedError: # this method throws error for e.g. SystemRandom instances, # so fall back to extracting 4k of state value = value.getrandbits(1 << 15) text = u("%s %s %s %.15f %.15f %s") % ( # if caller specified a seed value, mix it in value, # 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))`` # XXX: could reseed on every call 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' @deprecated_function(deprecated="1.7", removed="2.0", replacement="passlib.pwd.genword() / passlib.pwd.genphrase()") 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", "verify", "hash", "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.7.1/passlib/utils/handlers.py0000644000175000017500000031342613043701620021257 0ustar biscuitbiscuit00000000000000"""passlib.handler - code for implementing handlers, and global registry for handlers""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import inspect import logging; log = logging.getLogger(__name__) import math import threading from warnings import warn # site # pkg import passlib.exc as exc, passlib.ifc as ifc from passlib.exc import MissingBackendError, PasslibConfigWarning, \ PasslibHashWarning from passlib.ifc import PasswordHash from passlib.registry import get_crypt_handler from passlib.utils import ( consteq, getrandstr, getrandbytes, rng, to_native_str, is_crypt_handler, to_unicode, MAX_PASSWORD_SIZE, accepts_keyword, as_bool, update_mixin_classes) from passlib.utils.binary import ( BASE64_CHARS, HASH64_CHARS, PADDED_BASE64_CHARS, HEX_CHARS, UPPER_HEX_CHARS, LOWER_HEX_CHARS, ALL_BYTE_VALUES, ) from passlib.utils.compat import join_byte_values, irange, u, native_string_types, \ uascii_to_str, join_unicode, unicode, str_to_uascii, \ join_unicode, unicode_or_bytes_types, PY2, int_types from passlib.utils.decor import classproperty, deprecated_method # 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', # TODO: a bunch of other things are commonly assumed in this namespace # (e.g. HEX_CHARS etc); need to audit uses and update this list. ] #============================================================================= # constants #============================================================================= # deprecated aliases - will be removed after passlib 1.8 H64_CHARS = HASH64_CHARS B64_CHARS = BASE64_CHARS PADDED_B64_CHARS = PADDED_BASE64_CHARS UC_HEX_CHARS = UPPER_HEX_CHARS LC_HEX_CHARS = LOWER_HEX_CHARS #============================================================================= # support functions #============================================================================= def _bitsize(count, chars): """helper for bitsize() methods""" if chars and count: import math return int(count * math.log(len(chars), 2)) else: return 0 def guess_app_stacklevel(start=1): """ try to guess stacklevel for application warning. looks for first frame not part of passlib. """ frame = inspect.currentframe() count = -start try: while frame: name = frame.f_globals.get('__name__', "") if name.startswith("passlib.tests.") or not name.startswith("passlib."): return max(1, count) count += 1 frame = frame.f_back return start finally: del frame def warn_hash_settings_deprecation(handler, kwds): warn("passing settings to %(handler)s.hash() is deprecated, and won't be supported in Passlib 2.0; " "use '%(handler)s.using(**settings).hash(secret)' instead" % dict(handler=handler.name), DeprecationWarning, stacklevel=guess_app_stacklevel(2)) def extract_settings_kwds(handler, kwds): """ helper to extract settings kwds from mix of context & settings kwds. pops settings keys from kwds, returns them as a dict. """ context_keys = set(handler.context_kwds) return dict((key, kwds.pop(key)) for key in list(kwds) if key not in context_keys) #============================================================================= # parsing helpers #============================================================================= _UDOLLAR = u("$") _UZERO = u("0") def validate_secret(secret): """ensure secret has correct type & size""" if not isinstance(secret, unicode_or_bytes_types): raise exc.ExpectedStringError(secret, "secret") if len(secret) > MAX_PASSWORD_SIZE: raise exc.PasswordSizeError(MAX_PASSWORD_SIZE) 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 # def parse_mc3_long(hash, prefix, sep=_UDOLLAR, handler=None): # """ # parse hash using 3-part modular crypt format, # with complex settings string instead of simple rounds. # otherwise works same as :func:`parse_mc3` # """ # # 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: # return parts # elif len(parts) == 2: # settings, salt = parts # return settings, salt, None # else: # raise exc.MalformedHashError(handler) def parse_int(source, base=10, default=None, param="value", handler=None): """ helper to parse an integer config field :arg source: unicode source string :param base: numeric base :param default: optional default if source is empty :param param: name of variable, for error msgs :param handler: handler class, for error msgs """ if source.startswith(_UZERO) and source != _UZERO: raise exc.MalformedHashError(handler, "zero-padded %s field" % param) elif source: return int(source, base) elif default is None: raise exc.MalformedHashError(handler, "empty %s field" % param) else: return default #============================================================================= # 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)) #============================================================================= # parameter helpers #============================================================================= def validate_default_value(handler, default, norm, param="value"): """ assert helper that quickly validates default value. designed to get out of the way and reduce overhead when asserts are stripped. """ assert default is not None, "%s lacks default %s" % (handler.name, param) assert norm(default) == default, "%s: invalid default %s: %r" % (handler.name, param, default) return True def norm_integer(handler, value, min=1, max=None, # * param="value", relaxed=False): """ helper to normalize and validate an integer value (e.g. rounds, salt_size) :arg value: value provided to constructor :arg default: default value if none provided. if set to ``None``, value is required. :arg param: name of parameter (xxx: move to first arg?) :param min: minimum value (defaults to 1) :param max: maximum value (default ``None`` means no maximum) :returns: validated value """ # check type if not isinstance(value, int_types): raise exc.ExpectedTypeError(value, "integer", param) # check minimum if value < min: msg = "%s: %s (%d) is too low, must be at least %d" % (handler.name, param, value, min) if relaxed: warn(msg, exc.PasslibHashWarning) value = min else: raise ValueError(msg) # check maximum if max and value > max: msg = "%s: %s (%d) is too large, cannot be more than %d" % (handler.name, param, value, max) if relaxed: warn(msg, exc.PasslibHashWarning) value = max else: raise ValueError(msg) return value #============================================================================= # MinimalHandler #============================================================================= class MinimalHandler(PasswordHash): """ helper class for implementing hash handlers. provides nothing besides a base implementation of the .using() subclass constructor. """ #=================================================================== # class attr #=================================================================== #: private flag used by using() constructor to detect if this is already a subclass. _configured = False #=================================================================== # configuration interface #=================================================================== @classmethod def using(cls, relaxed=False): # NOTE: this provides the base implementation, which takes care of # creating the newly configured class. Mixins and subclasses # should wrap this, and modify the returned class to suit their options. # NOTE: 'relaxed' keyword is ignored here, but parsed so that subclasses # can check for it as argument, and modify their parsing behavior accordingly. name = cls.__name__ if not cls._configured: # TODO: straighten out class naming, repr, and .name attr name = "" % name return type(name, (cls,), dict(__module__=cls.__module__, _configured=True)) #=================================================================== # eoc #=================================================================== class TruncateMixin(MinimalHandler): """ PasswordHash mixin which provides a method that will check if secret would be truncated, and can be configured to throw an error. .. warning:: Hashers using this mixin will generally need to override the default PasswordHash.truncate_error policy of "True", and will similarly want to override .truncate_verify_reject as well. TODO: This should be done explicitly, but for now this mixin sets these flags implicitly. """ truncate_error = False truncate_verify_reject = False @classmethod def using(cls, truncate_error=None, **kwds): subcls = super(TruncateMixin, cls).using(**kwds) if truncate_error is not None: truncate_error = as_bool(truncate_error, param="truncate_error") if truncate_error is not None: subcls.truncate_error = truncate_error return subcls @classmethod def _check_truncate_policy(cls, secret): """ make sure secret won't be truncated. NOTE: this should only be called for .hash(), not for .verify(), which should honor the .truncate_verify_reject policy. """ assert cls.truncate_size is not None, "truncate_size must be set by subclass" if cls.truncate_error and len(secret) > cls.truncate_size: raise exc.PasswordTruncateError(cls) #============================================================================= # GenericHandler #============================================================================= class GenericHandler(MinimalHandler): """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:`hash`, 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 Placeholder checksum that will be used by genconfig() in lieu of actually generating a hash for the empty string. This should be a string of the same datatype as :attr:`checksum`. 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:: hash .. 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 # 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, **kwds): self.use_defaults = use_defaults super(GenericHandler, self).__init__(**kwds) if checksum is not None: # XXX: do we need to set .relaxed for checksum coercion? self.checksum = self._norm_checksum(checksum) # NOTE: would like to make this classmethod, but fshp checksum size # is dependant on .variant, so leaving this as instance method. def _norm_checksum(self, checksum, relaxed=False): """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. # normalize to bytes / unicode raw = self._checksum_is_bytes if raw: # NOTE: no clear route to reasonably 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 relaxed: warn("checksum should be unicode, not bytes", PasslibHashWarning) checksum = checksum.decode("ascii") else: raise exc.ExpectedTypeError(checksum, "unicode", "checksum") # 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 r""" 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: hash string with salt & digest included. should return native string type (ascii-bytes under python 2, unicode under python 3) """ raise NotImplementedError("%s must implement from_string()" % (self.__class__,)) #=================================================================== # checksum generation #=================================================================== # NOTE: this is only used by genconfig(), and will be removed in passlib 2.0 @property def _stub_checksum(self): """ placeholder used by default .genconfig() so it can avoid expense of calculating digest. """ # used fixed string if available if self.checksum_size: if self._checksum_is_bytes: return b'\x00' * self.checksum_size if self.checksum_chars: return self.checksum_chars[0] * self.checksum_size # hack to minimize cost of calculating real checksum if isinstance(self, HasRounds): orig = self.rounds self.rounds = self.min_rounds or 1 try: return self._calc_checksum("") finally: self.rounds = orig # final fallback, generate a real checksum return self._calc_checksum("") 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 hash(cls, secret, **kwds): if kwds: # Deprecating passing any settings keywords via .hash() as of passlib 1.7; everything # should use .using().hash() instead. If any keywords are specified, presume they're # context keywords by default (the common case), and extract out any settings kwds. # Support for passing settings via .hash() will be removed in Passlib 2.0, along with # this block of code. settings = extract_settings_kwds(cls, kwds) if settings: warn_hash_settings_deprecation(cls, settings) return cls.using(**settings).hash(secret, **kwds) # NOTE: at this point, 'kwds' should just contain context_kwds subset 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) #=================================================================== # legacy crypt interface #=================================================================== @deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls, **kwds): # NOTE: 'kwds' should generally always be settings, so after this completes, *should* be empty. settings = extract_settings_kwds(cls, kwds) if settings: return cls.using(**settings).genconfig(**kwds) # NOTE: this uses optional stub checksum to bypass potentially expensive digest generation, # when caller just wants the config string. self = cls(use_defaults=True, **kwds) self.checksum = self._stub_checksum return self.to_string() @deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config, **context): if config is None: raise TypeError("config must be string") validate_secret(secret) self = cls.from_string(config, **context) self.checksum = self._calc_checksum(secret) return self.to_string() #=================================================================== # migration interface (basde implementation) #=================================================================== @classmethod def needs_update(cls, hash, secret=None, **kwds): # NOTE: subclasses should generally just wrap _calc_needs_update() # to check their particular keywords. self = cls.from_string(hash) assert isinstance(self, cls) return self._calc_needs_update(secret=secret, **kwds) def _calc_needs_update(self, secret=None): """ internal helper for :meth:`needs_update`. """ # NOTE: this just provides a stub, subclasses & mixins # should override this with their own tests. return False #=================================================================== # 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) # XXX: make this a global function? @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.binary 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:`hash`: for most cases, if ``hash = cls.hash(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 hash() 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): return uascii_to_str(self._hash_prefix + self.checksum) # 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) # NOTE: passing 'config=None' here even though not currently allowed by ifc, # since it *is* allowed under the old 1.5 ifc we're checking for here. try: hash = wrapper_cls.genhash(secret, None, **context) except TypeError as err: if str(err) == "config must be string": raise NotImplementedError("%s must implement _calc_checksum()" % (cls,)) else: raise 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 hash(cls, secret, user=None, **context): return super(HasUserContext, cls).hash(secret, user=user, **context) @classmethod def verify(cls, secret, hash, user=None, **context): return super(HasUserContext, cls).verify(secret, hash, user=user, **context) @deprecated_method(deprecated="1.7", removed="2.0") @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 Methods ============= .. todo:: document using() and needs_update() options """ #=================================================================== # 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 #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, # keyword only... default_ident=None, ident=None, **kwds): """ This mixin adds support for the following :meth:`~passlib.ifc.PasswordHash.using` keywords: :param default_ident: default identifier that will be used by resulting customized hasher. :param ident: supported as alternate alias for **default_ident**. """ # resolve aliases if ident is not None: if default_ident is not None: raise TypeError("'default_ident' and 'ident' are mutually exclusive") default_ident = ident # create subclass subcls = super(HasManyIdents, cls).using(**kwds) # add custom default ident # (NOTE: creates instance to run value through _norm_ident()) if default_ident is not None: subcls.default_ident = cls(ident=default_ident, use_defaults=True).ident return subcls #=================================================================== # init #=================================================================== def __init__(self, ident=None, **kwds): super(HasManyIdents, self).__init__(**kwds) # init ident if ident is not None: ident = self._norm_ident(ident) elif self.use_defaults: ident = self.default_ident assert validate_default_value(self, ident, self._norm_ident, param="default_ident") else: raise TypeError("no ident specified") self.ident = ident @classmethod def _norm_ident(cls, ident): """ helper which normalizes & validates 'ident' value. """ # handle bytes assert ident is not None if isinstance(ident, bytes): ident = ident.decode('ascii') # check if identifier is valid iv = cls.ident_values if ident in iv: return ident # resolve aliases, and recheck against ident_values ia = cls.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 hash.startswith(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) # XXX: implement a needs_update() helper that marks everything but default_ident as deprecated? #=================================================================== # 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 ``0``. .. 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 = 0 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" # TODO: could support using(min/max_desired_salt_size) via using() and needs_update() #=================================================================== # instance attrs #=================================================================== salt = None #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, # keyword only... default_salt_size=None, salt_size=None, # aliases used by CryptContext salt=None, **kwds): # check for aliases used by CryptContext if salt_size is not None: if default_salt_size is not None: raise TypeError("'salt_size' and 'default_salt_size' aliases are mutually exclusive") default_salt_size = salt_size # generate new subclass subcls = super(HasSalt, cls).using(**kwds) # replace default_rounds relaxed = kwds.get("relaxed") if default_salt_size is not None: if isinstance(default_salt_size, native_string_types): default_salt_size = int(default_salt_size) subcls.default_salt_size = subcls._clip_to_valid_salt_size(default_salt_size, param="salt_size", relaxed=relaxed) # if salt specified, replace _generate_salt() with fixed output. # NOTE: this is mainly useful for testing / debugging. if salt is not None: salt = subcls._norm_salt(salt, relaxed=relaxed) subcls._generate_salt = staticmethod(lambda: salt) return subcls # XXX: would like to combine w/ _norm_salt() code below, but doesn't quite fit. @classmethod def _clip_to_valid_salt_size(cls, salt_size, param="salt_size", relaxed=True): """ internal helper -- clip salt size value to handler's absolute limits (min_salt_size / max_salt_size) :param relaxed: if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. if ``False``, raises a ValueError instead. :param param: optional name of parameter to insert into error/warning messages. :returns: clipped rounds value """ mn = cls.min_salt_size mx = cls.max_salt_size # check if salt size is fixed if mn == mx: if salt_size != mn: msg = "%s: %s (%d) must be exactly %d" % (cls.name, param, salt_size, mn) if relaxed: warn(msg, PasslibHashWarning) else: raise ValueError(msg) return mn # check min size if salt_size < mn: msg = "%s: %s (%r) below min_salt_size (%d)" % (cls.name, param, salt_size, mn) if relaxed: warn(msg, PasslibHashWarning) salt_size = mn else: raise ValueError(msg) # check max size if mx and salt_size > mx: msg = "%s: %s (%r) above max_salt_size (%d)" % (cls.name, param, salt_size, mx) if relaxed: warn(msg, PasslibHashWarning) salt_size = mx else: raise ValueError(msg) return salt_size #=================================================================== # init #=================================================================== def __init__(self, salt=None, **kwds): super(HasSalt, self).__init__(**kwds) if salt is not None: salt = self._parse_salt(salt) elif self.use_defaults: salt = self._generate_salt() assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,) else: raise TypeError("no salt specified") self.salt = salt # NOTE: split out mainly so sha256_crypt can subclass this def _parse_salt(self, salt): return self._norm_salt(salt) @classmethod def _norm_salt(cls, salt, relaxed=False): """helper to normalize & validate user-provided salt string :arg salt: salt string :raises TypeError: If salt not correct type. :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 salt """ # check type if cls._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 relaxed): salt = salt.decode("ascii") else: raise exc.ExpectedTypeError(salt, "unicode", "salt") # check charset sc = cls.salt_chars if sc is not None and any(c not in sc for c in salt): raise ValueError("invalid characters in %s salt" % cls.name) # check min size mn = cls.min_salt_size if mn and len(salt) < mn: msg = "salt too small (%s requires %s %d %s)" % (cls.name, "exactly" if mn == cls.max_salt_size else ">=", mn, cls._salt_unit) raise ValueError(msg) # check max size mx = cls.max_salt_size if mx and len(salt) > mx: msg = "salt too large (%s requires %s %d %s)" % (cls.name, "exactly" if mx == mn else "<=", mx, cls._salt_unit) if relaxed: warn(msg, PasslibHashWarning) salt = cls._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] @classmethod def _generate_salt(cls): """ helper method for _init_salt(); generates a new random salt string. """ return getrandstr(rng, cls.default_salt_chars, cls.default_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" @classmethod def _generate_salt(cls): assert cls.salt_chars in [None, ALL_BYTE_VALUES] return getrandbytes(rng, cls.default_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. Class Methods ============= .. todo:: document using() and needs_update() options 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 #=================================================================== #----------------- # algorithm options -- not application configurable #----------------- # XXX: rename to min_valid_rounds / max_valid_rounds, # to clarify role compared to min_desired_rounds / max_desired_rounds? min_rounds = 0 max_rounds = None rounds_cost = "linear" # default to the common case # hack to pass info to _CryptRecord (will be removed in passlib 2.0) using_rounds_kwds = ("min_desired_rounds", "max_desired_rounds", "min_rounds", "max_rounds", "default_rounds", "vary_rounds") #----------------- # desired & default rounds -- configurable via .using() classmethod #----------------- min_desired_rounds = None max_desired_rounds = None default_rounds = None vary_rounds = None #=================================================================== # instance attrs #=================================================================== rounds = None #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, # keyword only... min_desired_rounds=None, max_desired_rounds=None, default_rounds=None, vary_rounds=None, min_rounds=None, max_rounds=None, rounds=None, # aliases used by CryptContext **kwds): # check for aliases used by CryptContext if min_rounds is not None: if min_desired_rounds is not None: raise TypeError("'min_rounds' and 'min_desired_rounds' aliases are mutually exclusive") min_desired_rounds = min_rounds if max_rounds is not None: if max_desired_rounds is not None: raise TypeError("'max_rounds' and 'max_desired_rounds' aliases are mutually exclusive") max_desired_rounds = max_rounds # use 'rounds' as fallback for min, max, AND default # XXX: would it be better to make 'default_rounds' and 'rounds' # aliases, and have a separate 'require_rounds' parameter for this behavior? if rounds is not None: if min_desired_rounds is None: min_desired_rounds = rounds if max_desired_rounds is None: max_desired_rounds = rounds if default_rounds is None: default_rounds = rounds # generate new subclass subcls = super(HasRounds, cls).using(**kwds) # replace min_desired_rounds relaxed = kwds.get("relaxed") if min_desired_rounds is None: explicit_min_rounds = False min_desired_rounds = cls.min_desired_rounds else: explicit_min_rounds = True if isinstance(min_desired_rounds, native_string_types): min_desired_rounds = int(min_desired_rounds) subcls.min_desired_rounds = subcls._norm_rounds(min_desired_rounds, param="min_desired_rounds", relaxed=relaxed) # replace max_desired_rounds if max_desired_rounds is None: max_desired_rounds = cls.max_desired_rounds else: if isinstance(max_desired_rounds, native_string_types): max_desired_rounds = int(max_desired_rounds) if min_desired_rounds and max_desired_rounds < min_desired_rounds: msg = "%s: max_desired_rounds (%r) below min_desired_rounds (%r)" % \ (subcls.name, max_desired_rounds, min_desired_rounds) if explicit_min_rounds: raise ValueError(msg) else: warn(msg, PasslibConfigWarning) max_desired_rounds = min_desired_rounds subcls.max_desired_rounds = subcls._norm_rounds(max_desired_rounds, param="max_desired_rounds", relaxed=relaxed) # replace default_rounds if default_rounds is not None: if isinstance(default_rounds, native_string_types): default_rounds = int(default_rounds) if min_desired_rounds and default_rounds < min_desired_rounds: raise ValueError("%s: default_rounds (%r) below min_desired_rounds (%r)" % (subcls.name, default_rounds, min_desired_rounds)) elif max_desired_rounds and default_rounds > max_desired_rounds: raise ValueError("%s: default_rounds (%r) above max_desired_rounds (%r)" % (subcls.name, default_rounds, max_desired_rounds)) subcls.default_rounds = subcls._norm_rounds(default_rounds, param="default_rounds", relaxed=relaxed) # clip default rounds to new limits. if subcls.default_rounds is not None: subcls.default_rounds = subcls._clip_to_desired_rounds(subcls.default_rounds) # replace / set vary_rounds if vary_rounds is not None: if isinstance(vary_rounds, native_string_types): if vary_rounds.endswith("%"): vary_rounds = float(vary_rounds[:-1]) * 0.01 elif "." in vary_rounds: vary_rounds = float(vary_rounds) else: vary_rounds = int(vary_rounds) if vary_rounds < 0: raise ValueError("%s: vary_rounds (%r) below 0" % (subcls.name, vary_rounds)) elif isinstance(vary_rounds, float): # TODO: deprecate / disallow vary_rounds=1.0 if vary_rounds > 1: raise ValueError("%s: vary_rounds (%r) above 1.0" % (subcls.name, vary_rounds)) elif not isinstance(vary_rounds, int): raise TypeError("vary_rounds must be int or float") if vary_rounds: warn("The 'vary_rounds' option is deprecated as of Passlib 1.7, " "and will be removed in Passlib 2.0", PasslibConfigWarning) subcls.vary_rounds = vary_rounds # XXX: could cache _calc_vary_rounds_range() here if needed, # but would need to handle user manually changing .default_rounds return subcls @classmethod def _clip_to_desired_rounds(cls, rounds): """ helper for :meth:`_generate_rounds` -- clips rounds value to desired min/max set by class (if any) """ # NOTE: min/max_desired_rounds are None if unset. # check minimum mnd = cls.min_desired_rounds or 0 if rounds < mnd: return mnd # check maximum mxd = cls.max_desired_rounds if mxd and rounds > mxd: return mxd return rounds @classmethod def _calc_vary_rounds_range(cls, default_rounds): """ helper for :meth:`_generate_rounds` -- returns range for vary rounds generation. :returns: (lower, upper) limits suitable for random.randint() """ # XXX: could precalculate output of this in using() method, and save per-hash cost. # but then users patching cls.vary_rounds / cls.default_rounds would get wrong value. assert default_rounds vary_rounds = cls.vary_rounds # if vary_rounds specified as % of default, convert it to actual rounds def linear_to_native(value, upper): return value if isinstance(vary_rounds, float): assert 0 <= vary_rounds <= 1 # TODO: deprecate vary_rounds==1 if cls.rounds_cost == "log2": # special case -- have to convert default_rounds to linear scale, # apply +/- vary_rounds to that, and convert back to log scale again. # linear_to_native() takes care of the "convert back" step. default_rounds = 1 << default_rounds def linear_to_native(value, upper): if value <= 0: # log() undefined for <= 0 return 0 elif upper: # use smallest upper bound for start of range return int(math.log(value, 2)) else: # use greatest lower bound for end of range return int(math.ceil(math.log(value, 2))) # calculate integer vary rounds based on current default_rounds vary_rounds = int(default_rounds * vary_rounds) # calculate bounds based on default_rounds +/- vary_rounds assert vary_rounds >= 0 and isinstance(vary_rounds, int_types) lower = linear_to_native(default_rounds - vary_rounds, False) upper = linear_to_native(default_rounds + vary_rounds, True) return cls._clip_to_desired_rounds(lower), cls._clip_to_desired_rounds(upper) #=================================================================== # init #=================================================================== def __init__(self, rounds=None, **kwds): super(HasRounds, self).__init__(**kwds) if rounds is not None: rounds = self._parse_rounds(rounds) elif self.use_defaults: rounds = self._generate_rounds() assert self._norm_rounds(rounds) == rounds, "generated invalid rounds: %r" % (rounds,) else: raise TypeError("no rounds specified") self.rounds = rounds # NOTE: split out mainly so sha256_crypt & bsdi_crypt can subclass this def _parse_rounds(self, rounds): return self._norm_rounds(rounds) @classmethod def _norm_rounds(cls, rounds, relaxed=False, param="rounds"): """ helper for normalizing rounds value. :arg rounds: an integer cost parameter. :param relaxed: if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. if ``False``, raises a ValueError instead. :param param: optional name of parameter to insert into error/warning messages. :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 """ return norm_integer(cls, rounds, cls.min_rounds, cls.max_rounds, param=param, relaxed=relaxed) @classmethod def _generate_rounds(cls): """ internal helper for :meth:`_norm_rounds` -- returns default rounds value, incorporating vary_rounds, and any other limitations hash may place on rounds parameter. """ # load default rounds rounds = cls.default_rounds if rounds is None: raise TypeError("%s rounds value must be specified explicitly" % (cls.name,)) # randomly vary the rounds slightly basic on vary_rounds parameter. # reads default_rounds internally. if cls.vary_rounds: lower, upper = cls._calc_vary_rounds_range(rounds) assert lower <= rounds <= upper if lower < upper: rounds = rng.randint(lower, upper) return rounds #=================================================================== # migration interface #=================================================================== def _calc_needs_update(self, **kwds): """ mark hash as needing update if rounds is outside desired bounds. """ min_desired_rounds = self.min_desired_rounds if min_desired_rounds and self.rounds < min_desired_rounds: return True max_desired_rounds = self.max_desired_rounds if max_desired_rounds and self.rounds > max_desired_rounds: return True return super(HasRounds, self)._calc_needs_update(**kwds) #=================================================================== # experimental methods #=================================================================== @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 #=================================================================== #------------------------------------------------------------------------ # other common parameters #------------------------------------------------------------------------ class ParallelismMixin(GenericHandler): """ mixin which provides common behavior for 'parallelism' setting """ #=================================================================== # class attrs #=================================================================== # NOTE: subclasses should add "parallelism" to their settings_kwds #=================================================================== # instance attrs #=================================================================== #: parallelism setting (class-level value used as default) parallelism = 1 #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, parallelism=None, **kwds): subcls = super(ParallelismMixin, cls).using(**kwds) if parallelism is not None: if isinstance(parallelism, native_string_types): parallelism = int(parallelism) subcls.parallelism = subcls._norm_parallelism(parallelism, relaxed=kwds.get("relaxed")) return subcls #=================================================================== # init #=================================================================== def __init__(self, parallelism=None, **kwds): super(ParallelismMixin, self).__init__(**kwds) # init parallelism if parallelism is None: assert validate_default_value(self, self.parallelism, self._norm_parallelism, param="parallelism") else: self.parallelism = self._norm_parallelism(parallelism) @classmethod def _norm_parallelism(cls, parallelism, relaxed=False): return norm_integer(cls, parallelism, min=1, param="parallelism", relaxed=relaxed) #=================================================================== # hash migration #=================================================================== def _calc_needs_update(self, **kwds): """ mark hash as needing update if rounds is outside desired bounds. """ # XXX: for now, marking all hashes which don't have matching parallelism setting if self.parallelism != type(self).parallelism: return True return super(ParallelismMixin, self)._calc_needs_update(**kwds) #=================================================================== # eoc #=================================================================== #------------------------------------------------------------------------ # backend mixin & helpers #------------------------------------------------------------------------ #: global lock that must be held when changing backends. #: not bothering to make this more granular, as backend switching #: isn't a speed-critical path. lock is needed since there is some #: class-level state that may be modified during a "dry run" _backend_lock = threading.RLock() class BackendMixin(PasswordHash): """ PasswordHash mixin which provides generic framework for supporting multiple backends within the class. Public API ---------- .. 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). .. automethod:: get_backend .. automethod:: set_backend .. automethod:: has_backend .. warning:: :meth:`set_backend` is intended to be called during application startup -- it affects global state, and switching backends is not guaranteed threadsafe. Private API (Subclass Hooks) ---------------------------- Subclasses should set the :attr:`!backends` attribute to a tuple of the backends they wish to support. They should also define one method: .. classmethod:: _load_backend_{name}(dryrun=False) One copy of this method should be defined for each :samp:`name` within :attr:`!backends`. It will be called in order to load the backend, and should take care of whatever is needed to enable the backend. This may include importing modules, running tests, issuing warnings, etc. :param name: [Optional] name of backend. :param dryrun: [Optional] True/False if currently performing a "dry run". if True, the method should perform all setup actions *except* switching the class over to the new backend. :raises passlib.exc.PasslibSecurityError: if the backend is available, but cannot be loaded due to a security issue. :returns: False if backend not available, True if backend loaded. .. warning:: Due to the way passlib's internals are arranged, backends should generally store stateful data at the class level (not the module level), and be prepared to be called on subclasses which may be set to a different backend from their parent. (Idempotent module-level data such as lazy imports are fine). .. automethod:: _finalize_backend .. versionadded:: 1.7 """ #=================================================================== # class attrs #=================================================================== #: list of backend names, provided by subclass. backends = None #: private attr mixin uses to hold currently loaded backend (or ``None``) __backend = None #: optional class-specific text containing suggestion about what to do #: when no backends are available. _no_backend_suggestion = None #: shared attr used by set_backend() to indicate what backend it's loaded; #: meaningless while not in set_backend(). _pending_backend = None #: shared attr used by set_backend() to indicate if it's in "dry run" mode; #: meaningless while not in set_backend(). _pending_dry_run = False #=================================================================== # public api #=================================================================== @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 """ if not cls.__backend: cls.set_backend() assert cls.__backend, "set_backend() failed to load a default backend" return cls.__backend @classmethod def has_backend(cls, name="any"): """ Check if support is currently available for specified backend. :arg name: name of backend to check for. can be any string accepted by :meth:`set_backend`. :raises ValueError: if backend name is unknown :returns: * ``True`` if backend is available. * ``False`` if it's available / can't be loaded. * ``None`` if it's present, but won't load due to a security issue. """ try: cls.set_backend(name, dryrun=True) return True except (exc.MissingBackendError, exc.PasslibSecurityError): return False @classmethod def set_backend(cls, name="any", dryrun=False): """ Load specified backend. :arg name: name of backend to load, can be any of the following: * ``"any"`` -- use current backend if one is loaded, otherwise load the first available backend. * ``"default"`` -- use the first available backend. * any string in :attr:`backends`, loads specified backend. :param dryrun: If True, this perform all setup actions *except* switching over to the new backend. (this flag is used to implement :meth:`has_backend`). .. versionadded:: 1.7 :raises ValueError: If backend name is unknown. :raises passlib.exc.MissingBackendError: If specific backend is missing; or in the case of ``"any"`` / ``"default"``, if *no* backends are available. :raises passlib.exc.PasslibSecurityError: If ``"any"`` or ``"default"`` was specified, but the only backend available has a PasslibSecurityError. """ # check if active backend is acceptable if (name == "any" and cls.__backend) or (name and name == cls.__backend): return cls.__backend # if this isn't the final subclass, whose bases we can modify, # find that class, and recursively call this method for the proper class. owner = cls._get_backend_owner() if owner is not cls: return owner.set_backend(name, dryrun=dryrun) # pick first available backend if name == "any" or name == "default": default_error = None for name in cls.backends: try: return cls.set_backend(name, dryrun=dryrun) except exc.MissingBackendError: continue except exc.PasslibSecurityError as err: # backend is available, but refuses to load due to security issue. if default_error is None: default_error = err continue if default_error is None: msg = "%s: no backends available" % cls.name if cls._no_backend_suggestion: msg += cls._no_backend_suggestion default_error = exc.MissingBackendError(msg) raise default_error # validate name if name not in cls.backends: raise exc.UnknownBackendError(cls, name) # hand off to _set_backend() with _backend_lock: orig = cls._pending_backend, cls._pending_dry_run try: cls._pending_backend = name cls._pending_dry_run = dryrun cls._set_backend(name, dryrun) finally: cls._pending_backend, cls._pending_dry_run = orig if not dryrun: cls.__backend = name return name #=================================================================== # subclass hooks #=================================================================== @classmethod def _get_backend_owner(cls): """ return class that set_backend() should actually be modifying. for SubclassBackendMixin, this may not always be the class that was invoked. """ return cls @classmethod def _set_backend(cls, name, dryrun): """ Internal method invoked by :meth:`set_backend`. handles actual loading of specified backend. global _backend_lock will be held for duration of this method, and _pending_dry_run & _pending_backend will also be set. should return True / False. """ loader = cls._get_backend_loader(name) kwds = {} if accepts_keyword(loader, "name"): kwds['name'] = name if accepts_keyword(loader, "dryrun"): kwds['dryrun'] = dryrun ok = loader(**kwds) if ok is False: raise exc.MissingBackendError("%s: backend not available: %s" % (cls.name, name)) elif ok is not True: raise AssertionError("backend loaders must return True or False" ": %r" % (ok,)) @classmethod def _get_backend_loader(cls, name): """ Hook called to get the specified backend's loader. Should return callable which optionally takes ``"name"`` and/or ``"dryrun"`` keywords. Callable should return True if backend initialized successfully. If backend can't be loaded, callable should return False OR raise MissingBackendError directly. """ raise NotImplementedError("implement in subclass") @classmethod def _stub_requires_backend(cls): """ helper for subclasses to create stub methods which auto-load backend. """ if cls.__backend: raise AssertionError("%s: _finalize_backend(%r) failed to replace lazy loader" % (cls.name, cls.__backend)) cls.set_backend() if not cls.__backend: raise AssertionError("%s: set_backend() failed to load a default backend" % (cls.name)) #=================================================================== # eoc #=================================================================== class SubclassBackendMixin(BackendMixin): """ variant of BackendMixin which allows backends to be implemented as separate mixin classes, and dynamically switches them out. backend classes should implement a _load_backend() classmethod, which will be invoked with an optional 'dryrun' keyword, and should return True or False. _load_backend() will be invoked with ``cls`` equal to the mixin, *not* the overall class. .. versionadded:: 1.7 """ #=================================================================== # class attrs #=================================================================== # 'backends' required by BackendMixin #: NON-INHERITED flag that this class's bases should be modified by SubclassBackendMixin. #: should only be set to True in *one* subclass in hierarchy. _backend_mixin_target = False #: map of backend name -> mixin class _backend_mixin_map = None #=================================================================== # backend loading #=================================================================== @classmethod def _get_backend_owner(cls): """ return base class that we're actually switching backends on (needed in since backends frequently modify class attrs, and .set_backend may be called from a subclass). """ if not cls._backend_mixin_target: raise AssertionError("_backend_mixin_target not set") for base in cls.__mro__: if base.__dict__.get("_backend_mixin_target"): return base raise AssertionError("expected to find class w/ '_backend_mixin_target' set") @classmethod def _set_backend(cls, name, dryrun): # invoke backend loader (will throw error if fails) super(SubclassBackendMixin, cls)._set_backend(name, dryrun) # sanity check call args (should trust .set_backend, but will really # foul things up if this isn't the owner) assert cls is cls._get_backend_owner(), "_finalize_backend() not invoked on owner" # pick mixin class mixin_map = cls._backend_mixin_map assert mixin_map, "_backend_mixin_map not specified" mixin_cls = mixin_map[name] assert issubclass(mixin_cls, SubclassBackendMixin), "invalid mixin class" # modify to remove existing backend mixins, and insert the new one update_mixin_classes(cls, add=mixin_cls, remove=mixin_map.values(), append=True, before=SubclassBackendMixin, dryrun=dryrun, ) @classmethod def _get_backend_loader(cls, name): assert cls._backend_mixin_map, "_backend_mixin_map not specified" return cls._backend_mixin_map[name]._load_backend_mixin #=================================================================== # eoc #=================================================================== # XXX: rename to ChecksumBackendMixin? class HasManyBackends(BackendMixin, 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. .. versionchanged:: 1.7 This class now derives from :class:`BackendMixin`, which abstracts out a more generic framework for supporting multiple backends. The public api (:meth:`!get_backend`, :meth:`!has_backend`, :meth:`!set_backend`) is roughly the same. Private API (Subclass Hooks) ---------------------------- As of version 1.7, classes should implement :meth:`!_load_backend_{name}`, per :class:`BackendMixin`. This hook should invoke :meth:`!_set_calc_checksum_backcend` to install it's backend method. .. deprecated:: 1.7 The following api is deprecated, and will be removed in Passlib 2.0: .. 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`. """ #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): "wrapper for backend, for common code""" # NOTE: not overwriting _calc_checksum() directly, so that classes can provide # common behavior in that method, # and then invoke _calc_checksum_backend() to do the work. return self._calc_checksum_backend(secret) def _calc_checksum_backend(self, secret): """ stub for _calc_checksum_backend() -- should load backend if one hasn't been loaded; if one has been loaded, this method should have been monkeypatched by _finalize_backend(). """ self._stub_requires_backend() return self._calc_checksum_backend(secret) #=================================================================== # BackendMixin hooks #=================================================================== @classmethod def _get_backend_loader(cls, name): """ subclassed to support legacy 1.6 HasManyBackends api. (will be removed in passlib 2.0) """ # check for 1.7 loader loader = getattr(cls, "_load_backend_" + name, None) if loader is None: # fallback to pre-1.7 _has_backend_xxx + _calc_checksum_xxx() api def loader(): return cls.__load_legacy_backend(name) else: # make sure 1.6 api isn't defined at same time assert not hasattr(cls, "_has_backend_" + name), ( "%s: can't specify both ._load_backend_%s() " "and ._has_backend_%s" % (cls.name, name, name) ) return loader @classmethod def __load_legacy_backend(cls, name): value = getattr(cls, "_has_backend_" + name) warn("%s: support for ._has_backend_%s is deprecated as of Passlib 1.7, " "and will be removed in Passlib 1.9/2.0, please implement " "._load_backend_%s() instead" % (cls.name, name, name), DeprecationWarning, ) if value: func = getattr(cls, "_calc_checksum_" + name) cls._set_calc_checksum_backend(func) return True else: return False @classmethod def _set_calc_checksum_backend(cls, func): """ helper used by subclasses to validate & set backend-specific calc checksum helper. """ backend = cls._pending_backend assert backend, "should only be called during set_backend()" if not callable(func): raise RuntimeError("%s: backend %r returned invalid callable: %r" % (cls.name, backend, func)) if not cls._pending_dry_run: cls._calc_checksum_backend = func #=================================================================== # eoc #=================================================================== #============================================================================= # wrappers #============================================================================= # XXX: should this inherit from PasswordHash? 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. """ #: list of attributes which should be cloned by .using() _using_clone_attrs = () 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._set_wrapped(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 _set_wrapped(self, handler): # check this is a valid 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) # store reference self._wrapped_handler = handler def _get_wrapped(self): handler = self._wrapped_handler if handler is None: handler = get_crypt_handler(self._wrapped_name) self._set_wrapped(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 = tuple(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 # XXX: change this to proxy everything that doesn't start with "_"? _proxy_attrs = ( "setting_kwds", "context_kwds", "default_rounds", "min_rounds", "max_rounds", "rounds_cost", "min_desired_rounds", "max_desired_rounds", "vary_rounds", "default_salt_size", "min_salt_size", "max_salt_size", "salt_chars", "default_salt_chars", "backends", "has_backend", "get_backend", "set_backend", "is_disabled", "truncate_size", "truncate_error", "truncate_verify_reject", # internal info attrs needed for test inspection "_salt_is_bytes", ) 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 __setattr__(self, attr, value): # if proxy attr present on wrapped object, # and we own it, modify *it* instead. # TODO: needs UTs # TODO: any other cases where wrapped is "owned"? # currently just if created via .using() if attr in self._proxy_attrs and self._derived_from: wrapped = self.wrapped if hasattr(wrapped, attr): setattr(wrapped, attr, value) return return object.__setattr__(self, attr, value) 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) #: set by _using(), helper for test harness' handler_derived_from() _derived_from = None def using(self, **kwds): # generate subclass of wrapped handler subcls = self.wrapped.using(**kwds) assert subcls is not self.wrapped # then create identical wrapper which wraps the new subclass. wrapper = PrefixWrapper(self.name, subcls, prefix=self.prefix, orig_prefix=self.orig_prefix) wrapper._derived_from = self for attr in self._using_clone_attrs: setattr(wrapper, attr, getattr(self, attr)) return wrapper def needs_update(self, hash, **kwds): hash = self._unwrap_hash(hash) return self.wrapped.needs_update(hash, **kwds) 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) @deprecated_method(deprecated="1.7", removed="2.0") def genconfig(self, **kwds): config = self.wrapped.genconfig(**kwds) if config is None: raise RuntimeError(".genconfig() must return a string, not None") return self._wrap_hash(config) @deprecated_method(deprecated="1.7", removed="2.0") def genhash(self, secret, config, **kwds): # TODO: under 2.0, throw TypeError if config is None, rather than passing it through 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)) @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") def encrypt(self, secret, **kwds): return self.hash(secret, **kwds) def hash(self, secret, **kwds): return self._wrap_hash(self.wrapped.hash(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.7.1/passlib/utils/decor.py0000644000175000017500000001674313043410411020550 0ustar biscuitbiscuit00000000000000""" passlib.utils.decor -- helper decorators & properties """ #============================================================================= # imports #============================================================================= # core from __future__ import absolute_import, division, print_function import logging log = logging.getLogger(__name__) from functools import wraps, update_wrapper import types from warnings import warn # site # pkg from passlib.utils.compat import PY3 # local __all__ = [ "classproperty", "hybrid_method", "memoize_single_value", "memoized_property", "deprecated_function", "deprecated_method", ] #============================================================================= # class-level decorators #============================================================================= 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 class hybrid_method(object): """ decorator which invokes function with class if called as class method, and with object if called at instance level. """ def __init__(self, func): self.func = func update_wrapper(self, func) def __get__(self, obj, cls): if obj is None: obj = cls if PY3: return types.MethodType(self.func, obj) else: return types.MethodType(self.func, obj, cls) #============================================================================= # memoization #============================================================================= def memoize_single_value(func): """ decorator for function which takes no args, and memoizes result. exposes a ``.clear_cache`` method to clear the cached value. """ cache = {} @wraps(func) def wrapper(): try: return cache[True] except KeyError: pass value = cache[True] = func() return value def clear_cache(): cache.pop(True, None) wrapper.clear_cache = clear_cache return wrapper class memoized_property(object): """ decorator which invokes method once, then replaces attr with result """ def __init__(self, func): self.__func__ = func self.__name__ = func.__name__ self.__doc__ = func.__doc__ def __get__(self, obj, cls): if obj is None: return self value = self.__func__(obj) setattr(obj, self.__name__, value) return value if not PY3: @property def im_func(self): """py2 alias""" return self.__func__ def clear_cache(self, obj): """ class-level helper to clear stored value (if any). usage: :samp:`type(self).{attr}.clear_cache(self)` """ obj.__dict__.pop(self.__name__, None) def peek_cache(self, obj, default=None): """ class-level helper to peek at stored value usage: :samp:`value = type(self).{attr}.clear_cache(self)` """ return obj.__dict__.get(self.__name__, default) # 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" #============================================================================= # deprecation #============================================================================= def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True, replacement=None, _is_method=False, func_module=None): """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): is_classmethod = _is_method and isinstance(func, classmethod) if is_classmethod: # NOTE: PY26 doesn't support "classmethod().__func__" directly... func = func.__get__(None, type).__func__ opts = dict( mod=func_module or func.__module__, name=func.__name__, deprecated=deprecated, removed=removed, ) if _is_method: def wrapper(*args, **kwds): tmp = opts.copy() klass = args[0] if is_classmethod else 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,) if is_classmethod: wrapper = classmethod(wrapper) 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) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/utils/binary.py0000644000175000017500000007527613015205366020760 0ustar biscuitbiscuit00000000000000""" passlib.utils.binary - binary data encoding/decoding/manipulation """ #============================================================================= # imports #============================================================================= # core from __future__ import absolute_import, division, print_function from base64 import ( b64encode, b64decode, b32decode as _b32decode, b32encode as _b32encode, ) from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError import logging log = logging.getLogger(__name__) # site # pkg from passlib import exc from passlib.utils.compat import ( PY3, bascii_to_str, irange, imap, iter_byte_chars, join_byte_values, join_byte_elems, nextgetter, suppress_cause, u, unicode, unicode_or_bytes_types, ) from passlib.utils.decor import memoized_property # from passlib.utils import BASE64_CHARS, HASH64_CHARS # local __all__ = [ # constants "BASE64_CHARS", "PADDED_BASE64_CHARS", "AB64_CHARS", "HASH64_CHARS", "BCRYPT_CHARS", "HEX_CHARS", "LOWER_HEX_CHARS", "UPPER_HEX_CHARS", "ALL_BYTE_VALUES", # misc "compile_byte_translation", # base64 'ab64_encode', 'ab64_decode', 'b64s_encode', 'b64s_decode', # base32 "b32encode", "b32decode", # custom encodings 'Base64Engine', 'LazyBase64Engine', 'h64', 'h64big', 'bcrypt64', ] #============================================================================= # constant strings #============================================================================= #------------------------------------------------------------- # common salt_chars & checksum_chars values #------------------------------------------------------------- #: standard base64 charmap BASE64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") #: alt base64 charmap -- "." instead of "+" AB64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./") #: charmap used by HASH64 encoding. HASH64_CHARS = u("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") #: charmap used by BCrypt BCRYPT_CHARS = u("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") #: std base64 chars + padding char PADDED_BASE64_CHARS = BASE64_CHARS + u("=") #: all hex chars HEX_CHARS = u("0123456789abcdefABCDEF") #: upper case hex chars UPPER_HEX_CHARS = u("0123456789ABCDEF") #: lower case hex chars LOWER_HEX_CHARS = u("0123456789abcdef") #------------------------------------------------------------- # byte strings #------------------------------------------------------------- #: special byte string containing all possible byte values #: NOTE: for efficiency, this is treated as singleton by some of the code ALL_BYTE_VALUES = join_byte_values(irange(256)) #: some string constants we reuse B_EMPTY = b'' B_NULL = b'\x00' B_EQUAL = b'=' #============================================================================= # byte translation #============================================================================= #: base list used to compile byte translations _TRANSLATE_SOURCE = list(iter_byte_chars(ALL_BYTE_VALUES)) def compile_byte_translation(mapping, source=None): """ return a 256-byte string for translating bytes using specified mapping. bytes not specified by mapping will be left alone. :param mapping: dict mapping input byte (str or int) -> output byte (str or int). :param source: optional existing byte translation string to use as base. (must be 255-length byte string). defaults to identity mapping. :returns: 255-length byte string for passing to bytes().translate. """ if source is None: target = _TRANSLATE_SOURCE[:] else: assert isinstance(source, bytes) and len(source) == 255 target = list(iter_byte_chars(source)) for k, v in mapping.items(): if isinstance(k, unicode_or_bytes_types): k = ord(k) assert isinstance(k, int) and 0 <= k < 256 if isinstance(v, unicode): v = v.encode("ascii") assert isinstance(v, bytes) and len(v) == 1 target[k] = v return B_EMPTY.join(target) #============================================================================= # unpadding / stripped base64 encoding #============================================================================= def b64s_encode(data): """ encode using shortened base64 format which omits padding & whitespace. uses default ``+/`` altchars. """ return b2a_base64(data).rstrip(_BASE64_STRIP) def b64s_decode(data): """ decode from shortened base64 format which omits padding & whitespace. uses default ``+/`` altchars. """ if isinstance(data, unicode): # needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64() try: data = data.encode("ascii") except UnicodeEncodeError: raise suppress_cause(ValueError("string argument should contain only ASCII characters")) off = len(data) & 3 if off == 0: pass elif off == 2: data += _BASE64_PAD2 elif off == 3: data += _BASE64_PAD1 else: # off == 1 raise ValueError("invalid base64 input") try: return a2b_base64(data) except _BinAsciiError as err: raise suppress_cause(TypeError(err)) #============================================================================= # adapted-base64 encoding #============================================================================= _BASE64_STRIP = b"=\n" _BASE64_PAD1 = b"=" _BASE64_PAD2 = b"==" # XXX: Passlib 1.8/1.9 -- deprecate everything that's using ab64_encode(), # have it start outputing b64s_encode() instead? can use a64_decode() to retain backwards compat. def ab64_encode(data): """ encode using shortened base64 format which omits padding & whitespace. uses custom ``./`` altchars. it is primarily used by Passlib's custom pbkdf2 hashes. """ return b64s_encode(data).replace(b"+", b".") def ab64_decode(data): """ decode from shortened base64 format which omits padding & whitespace. uses custom ``./`` altchars, but supports decoding normal ``+/`` altchars as well. it is primarily used by Passlib's custom pbkdf2 hashes. """ if isinstance(data, unicode): # needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64() try: data = data.encode("ascii") except UnicodeEncodeError: raise suppress_cause(ValueError("string argument should contain only ASCII characters")) return b64s_decode(data.replace(b".", b"+")) #============================================================================= # base32 codec #============================================================================= def b32encode(source): """ wrapper around :func:`base64.b32encode` which strips padding, and returns a native string. """ # NOTE: using upper case by default here, since 'I & L' are less # visually ambiguous than 'i & l' return bascii_to_str(_b32encode(source).rstrip(B_EQUAL)) #: byte translation map to replace common mistyped base32 chars. #: XXX: could correct '1' -> 'I', but could be a mistyped lower-case 'l', so leaving it alone. _b32_translate = compile_byte_translation({"8": "B", "0": "O"}) #: helper to add padding _b32_decode_pad = B_EQUAL * 8 def b32decode(source): """ wrapper around :func:`base64.b32decode` which handles common mistyped chars. padding optional, ignored if present. """ # encode & correct for typos if isinstance(source, unicode): source = source.encode("ascii") source = source.translate(_b32_translate) # pad things so final string is multiple of 8 remainder = len(source) & 0x7 if remainder: source += _b32_decode_pad[:-remainder] # XXX: py27 stdlib's version of this has some inefficiencies, # could look into using optimized version. return _b32decode(source, True) #============================================================================= # 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 exc.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 = nextgetter(iter(source)) else: next_value = nextgetter(ord(elem) for elem in source) 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 = nextgetter(imap(self._decode64, source)) try: return join_byte_values(self._decode_bytes(next_value, chunks, tail)) except KeyError as 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_int30(self, source): """decode 5 char string -> 30 bit integer""" return self._decode_int(source, 30) 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_int30(self, value): """decode 5 char string -> 30 bit integer""" if value < 0 or value > 0x3fffffff: raise ValueError("value out of range") return self._encode_int(value, 30) 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 variants #------------------------------------------------------------- h64 = LazyBase64Engine(HASH64_CHARS) h64big = LazyBase64Engine(HASH64_CHARS, big=True) bcrypt64 = LazyBase64Engine(BCRYPT_CHARS, big=True) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/utils/pbkdf2.py0000644000175000017500000001526013015205366020627 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 #============================================================================= from __future__ import division # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib.exc import ExpectedTypeError from passlib.utils.decor import deprecated_function from passlib.utils.compat import native_string_types from passlib.crypto.digest import norm_hash_name, lookup_hash, pbkdf1 as _pbkdf1, pbkdf2_hmac, compile_hmac # local __all__ = [ # hash utils "norm_hash_name", # prf utils "get_prf", # kdfs "pbkdf1", "pbkdf2", ] #============================================================================= # issue deprecation warning for module #============================================================================= from warnings import warn warn("the module 'passlib.utils.pbkdf2' is deprecated as of Passlib 1.7, " "and will be removed in Passlib 2.0, please use 'passlib.crypto' instead", DeprecationWarning) #============================================================================= # hash helpers #============================================================================= norm_hash_name = deprecated_function(deprecated="1.7", removed="1.8", func_module=__name__, replacement="passlib.crypto.digest.norm_hash_name")(norm_hash_name) #============================================================================= # prf lookup #============================================================================= #: cache mapping prf name/func -> (func, digest_size) _prf_cache = {} #: list of accepted prefixes _HMAC_PREFIXES = ("hmac_", "hmac-") 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. todo: restore text about callables. :raises ValueError: if the name is not known :raises TypeError: if the name is not a callable or string :returns: a tuple of :samp:`({prf_func}, {digest_size})`, where: * :samp:`{prf_func}` is a function implementing the specified PRF, and has the signature ``prf_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') .. deprecated:: 1.7 This function is deprecated, and will be removed in Passlib 2.0. This only related replacement is :func:`passlib.crypto.digest.compile_hmac`. """ global _prf_cache if name in _prf_cache: return _prf_cache[name] if isinstance(name, native_string_types): if not name.startswith(_HMAC_PREFIXES): raise ValueError("unknown prf algorithm: %r" % (name,)) digest = lookup_hash(name[5:]).name def hmac(key, msg): return compile_hmac(digest, key)(msg) record = (hmac, hmac.digest_info.digest_size) elif callable(name): # assume it's a callable, use it directly digest_size = len(name(b'x', b'y')) record = (name, digest_size) else: raise ExpectedTypeError(name, "str or callable", "prf name") _prf_cache[name] = record return record #============================================================================= # 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. .. deprecated:: 1.7 This has been relocated to :func:`passlib.crypto.digest.pbkdf1`, and this version will be removed in Passlib 2.0. *Note the call signature has changed.* """ return _pbkdf1(hash, secret, salt, rounds, keylen) #============================================================================= # pbkdf2 #============================================================================= 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 must be a string starting with ``"hmac-"``, followed by the name of a known digest. this defaults to ``"hmac-sha1"`` (the only prf explicitly listed in the PBKDF2 specification) .. rst-class:: warning .. versionchanged 1.7: This argument no longer supports arbitrary PRF callables -- These were rarely / never used, and created too many unwanted codepaths. :returns: raw bytes of generated key .. deprecated:: 1.7 This has been deprecated in favor of :func:`passlib.crypto.digest.pbkdf2_hmac`, and will be removed in Passlib 2.0. *Note the call signature has changed.* """ if callable(prf) or (isinstance(prf, native_string_types) and not prf.startswith(_HMAC_PREFIXES)): raise NotImplementedError("non-HMAC prfs are not supported as of Passlib 1.7") digest = prf[5:] return pbkdf2_hmac(digest, secret, salt, rounds, keylen) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/utils/compat/0000755000175000017500000000000013043774617020377 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/utils/compat/__init__.py0000644000175000017500000003221713015205366022502 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) # make sure it's not an unsupported version, even if we somehow got this far if sys.version_info < (2,6) or (3,0) <= sys.version_info < (3,2): raise RuntimeError("Passlib requires Python 2.6, 2.7, or >= 3.2 (as of passlib 1.7)") PY26 = sys.version_info < (2,7) #------------------------------------------------------------------------ # python implementation #------------------------------------------------------------------------ JYTHON = sys.platform.startswith('java') PYPY = hasattr(sys, "pypy_version_info") if PYPY and sys.pypy_version_info < (2,0): raise RuntimeError("passlib requires pypy >= 2.0 (as of passlib 1.7)") # e.g. '2.7.7\n[Pyston 0.5.1]' PYSTON = "Pyston" in sys.version #============================================================================= # 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', 'PY26', # io 'BytesIO', 'StringIO', 'NativeStringIO', 'SafeConfigParser', 'print_', # type detection ## 'is_mapping', 'int_types', 'num_types', 'unicode_or_bytes_types', 'native_string_types', # unicode/bytes types & helpers 'u', 'unicode', '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', # collections 'OrderedDict', # introspection '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 # TODO: once we drop python 3.2 support, can use u'' again! def u(s): assert isinstance(s, str) return s unicode_or_bytes_types = (str, bytes) native_string_types = (unicode,) else: unicode = builtins.unicode def u(s): assert isinstance(s, str) return s.decode("unicode_escape") unicode_or_bytes_types = (basestring,) native_string_types = (basestring,) # shorter preferred aliases unicode_or_bytes = unicode_or_bytes_types unicode_or_str = native_string_types # unicode -- unicode type, regardless of python version # bytes -- bytes type, regardless of python version # unicode_or_bytes_types -- types that text can occur in, whether encoded or not # native_string_types -- types that native python strings (dict keys etc) can occur in. #============================================================================= # 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() def nextgetter(obj): return obj.__next__ izip = zip else: irange = xrange ##lrange = range lmap = map from itertools import imap, izip def iteritems(d): return d.iteritems() def itervalues(d): return d.itervalues() def nextgetter(obj): return obj.next add_doc(nextgetter, "return function that yields successive values from iterable") #============================================================================= # typing #============================================================================= ##def is_mapping(obj): ## # non-exhaustive check, enough to distinguish from lists, etc ## return hasattr(obj, "items") #============================================================================= # introspection #============================================================================= 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) def get_unbound_method_function(func): """given unbound method, return underlying function""" return func if PY3 else func.__func__ def suppress_cause(exc): """ backward compat hack to suppress exception cause in python3.3+ one python < 3.3 support is dropped, can replace all uses with "raise exc from None" """ exc.__cause__ = None return exc #============================================================================= # input/output #============================================================================= if PY3: _lazy_attrs = dict( BytesIO="io.BytesIO", UnicodeIO="io.StringIO", NativeStringIO="io.StringIO", 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, unicode_or_bytes_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, unicode_or_bytes_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) #============================================================================= # collections #============================================================================= if PY26: _lazy_attrs['OrderedDict'] = 'passlib.utils.compat._ordered_dict.OrderedDict' else: _lazy_attrs['OrderedDict'] = 'collections.OrderedDict' #============================================================================= # 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.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.7.1/passlib/utils/compat/_ordered_dict.py0000644000175000017500000002026013015205366023524 0ustar biscuitbiscuit00000000000000"""passlib.utils.compat._ordered_dict -- backport of collections.OrderedDict for py26 taken from stdlib-suggested recipe at http://code.activestate.com/recipes/576693/ this should be imported from passlib.utils.compat.OrderedDict, not here. """ try: from thread import get_ident as _get_ident except ImportError: from dummy_thread import get_ident as _get_ident class OrderedDict(dict): """Dictionary that remembers insertion order""" # An inherited dict maps keys to values. # The inherited dict provides __getitem__, __len__, __contains__, and get. # The remaining methods are order-aware. # Big-O running times for all methods are the same as for regular dictionaries. # The internal self.__map dictionary maps keys to links in a doubly linked list. # The circular doubly linked list starts and ends with a sentinel element. # The sentinel element never gets deleted (this simplifies the algorithm). # Each link is stored as a list of length three: [PREV, NEXT, KEY]. def __init__(self, *args, **kwds): '''Initialize an ordered dictionary. Signature is the same as for regular dictionaries, but keyword arguments are not recommended because their insertion order is arbitrary. ''' if len(args) > 1: raise TypeError('expected at most 1 arguments, got %d' % len(args)) try: self.__root except AttributeError: self.__root = root = [] # sentinel node root[:] = [root, root, None] self.__map = {} self.__update(*args, **kwds) def __setitem__(self, key, value, dict_setitem=dict.__setitem__): 'od.__setitem__(i, y) <==> od[i]=y' # Setting a new item creates a new link which goes at the end of the linked # list, and the inherited dictionary is updated with the new key/value pair. if key not in self: root = self.__root last = root[0] last[1] = root[0] = self.__map[key] = [last, root, key] dict_setitem(self, key, value) def __delitem__(self, key, dict_delitem=dict.__delitem__): 'od.__delitem__(y) <==> del od[y]' # Deleting an existing item uses self.__map to find the link which is # then removed by updating the links in the predecessor and successor nodes. dict_delitem(self, key) link_prev, link_next, key = self.__map.pop(key) link_prev[1] = link_next link_next[0] = link_prev def __iter__(self): 'od.__iter__() <==> iter(od)' root = self.__root curr = root[1] while curr is not root: yield curr[2] curr = curr[1] def __reversed__(self): 'od.__reversed__() <==> reversed(od)' root = self.__root curr = root[0] while curr is not root: yield curr[2] curr = curr[0] def clear(self): 'od.clear() -> None. Remove all items from od.' try: for node in self.__map.itervalues(): del node[:] root = self.__root root[:] = [root, root, None] self.__map.clear() except AttributeError: pass dict.clear(self) def popitem(self, last=True): '''od.popitem() -> (k, v), return and remove a (key, value) pair. Pairs are returned in LIFO order if last is true or FIFO order if false. ''' if not self: raise KeyError('dictionary is empty') root = self.__root if last: link = root[0] link_prev = link[0] link_prev[1] = root root[0] = link_prev else: link = root[1] link_next = link[1] root[1] = link_next link_next[0] = root key = link[2] del self.__map[key] value = dict.pop(self, key) return key, value # -- the following methods do not depend on the internal structure -- def keys(self): 'od.keys() -> list of keys in od' return list(self) def values(self): 'od.values() -> list of values in od' return [self[key] for key in self] def items(self): 'od.items() -> list of (key, value) pairs in od' return [(key, self[key]) for key in self] def iterkeys(self): 'od.iterkeys() -> an iterator over the keys in od' return iter(self) def itervalues(self): 'od.itervalues -> an iterator over the values in od' for k in self: yield self[k] def iteritems(self): 'od.iteritems -> an iterator over the (key, value) items in od' for k in self: yield (k, self[k]) def update(*args, **kwds): '''od.update(E, **F) -> None. Update od from dict/iterable E and F. If E is a dict instance, does: for k in E: od[k] = E[k] If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] Or if E is an iterable of items, does: for k, v in E: od[k] = v In either case, this is followed by: for k, v in F.items(): od[k] = v ''' if len(args) > 2: raise TypeError('update() takes at most 2 positional ' 'arguments (%d given)' % (len(args),)) elif not args: raise TypeError('update() takes at least 1 argument (0 given)') self = args[0] # Make progressively weaker assumptions about "other" other = () if len(args) == 2: other = args[1] if isinstance(other, dict): for key in other: self[key] = other[key] elif hasattr(other, 'keys'): for key in other.keys(): self[key] = other[key] else: for key, value in other: self[key] = value for key, value in kwds.items(): self[key] = value __update = update # let subclasses override update without breaking __init__ __marker = object() def pop(self, key, default=__marker): '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. If key is not found, d is returned if given, otherwise KeyError is raised. ''' if key in self: result = self[key] del self[key] return result if default is self.__marker: raise KeyError(key) return default def setdefault(self, key, default=None): 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' if key in self: return self[key] self[key] = default return default def __repr__(self, _repr_running={}): 'od.__repr__() <==> repr(od)' call_key = id(self), _get_ident() if call_key in _repr_running: return '...' _repr_running[call_key] = 1 try: if not self: return '%s()' % (self.__class__.__name__,) return '%s(%r)' % (self.__class__.__name__, self.items()) finally: del _repr_running[call_key] def __reduce__(self): 'Return state information for pickling' items = [[k, self[k]] for k in self] inst_dict = vars(self).copy() for k in vars(OrderedDict()): inst_dict.pop(k, None) if inst_dict: return (self.__class__, (items,), inst_dict) return self.__class__, (items,) def copy(self): 'od.copy() -> a shallow copy of od' return self.__class__(self) @classmethod def fromkeys(cls, iterable, value=None): '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S and values equal to v (which defaults to None). ''' d = cls() for key in iterable: d[key] = value return d def __eq__(self, other): '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive while comparison to a regular mapping is order-insensitive. ''' if isinstance(other, OrderedDict): return len(self)==len(other) and self.items() == other.items() return dict.__eq__(self, other) def __ne__(self, other): return not self == other passlib-1.7.1/passlib/utils/des.py0000644000175000017500000000416313015205366020232 0ustar biscuitbiscuit00000000000000""" passlib.utils.des - DEPRECATED LOCATION, WILL BE REMOVED IN 2.0 This has been moved to :mod:`passlib.crypto.des`. """ #============================================================================= # import from new location #============================================================================= from warnings import warn warn("the 'passlib.utils.des' module has been relocated to 'passlib.crypto.des' " "as of passlib 1.7, and the old location will be removed in passlib 2.0", DeprecationWarning) #============================================================================= # relocated functions #============================================================================= from passlib.utils.decor import deprecated_function from passlib.crypto.des import expand_des_key, des_encrypt_block, des_encrypt_int_block expand_des_key = deprecated_function(deprecated="1.7", removed="1.8", replacement="passlib.crypto.des.expand_des_key")(expand_des_key) des_encrypt_block = deprecated_function(deprecated="1.7", removed="1.8", replacement="passlib.crypto.des.des_encrypt_block")(des_encrypt_block) des_encrypt_int_block = deprecated_function(deprecated="1.7", removed="1.8", replacement="passlib.crypto.des.des_encrypt_int_block")(des_encrypt_int_block) #============================================================================= # deprecated functions -- not carried over to passlib.crypto.des #============================================================================= import struct _unpack_uint64 = struct.Struct(">Q").unpack @deprecated_function(deprecated="1.6", removed="1.8", replacement="passlib.crypto.des.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 = _unpack_uint64(key)[0] return des_encrypt_int_block(key, input, salt, rounds) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/hosts.py0000644000175000017500000000634613015205366017464 0ustar biscuitbiscuit00000000000000"""passlib.hosts""" #============================================================================= # imports #============================================================================= # core from warnings import warn # pkg from passlib.context import LazyCryptContext from passlib.exc import PasslibRuntimeWarning from passlib import registry 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 registry.os_crypt_present: # NOTE: this is basically mimicing the output of os crypt(), # except that it uses passlib's (usually stronger) defaults settings, # and can be inspected and used much more flexibly. def _iter_os_crypt_schemes(): """helper which iterates over supported os_crypt schemes""" out = registry.get_supported_os_crypt_schemes() if out: # only offer disabled handler if there's another scheme in front, # as this can't actually hash any passwords out += ("unix_disabled",) return out 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.7.1/passlib/exc.py0000644000175000017500000002647213043701620017100 0ustar biscuitbiscuit00000000000000"""passlib.exc -- exceptions & warnings raised by passlib""" #============================================================================= # exceptions #============================================================================= class UnknownBackendError(ValueError): """ Error raised if multi-backend handler doesn't recognize backend name. Inherits from :exc:`ValueError`. .. versionadded:: 1.7 """ def __init__(self, hasher, backend): self.hasher = hasher self.backend = backend message = "%s: unknown backend: %r" % (hasher.name, backend) ValueError.__init__(self, message) 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 (by default, 4096 characters); or if password exceeds a hash-specific size limitation. 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. .. attribute:: max_size indicates the maximum allowed size. .. versionadded:: 1.6 """ max_size = None def __init__(self, max_size, msg=None): self.max_size = max_size if msg is None: msg = "password exceeds maximum allowed size" ValueError.__init__(self, msg) # this also prevents a glibc crypt segfault issue, detailed here ... # http://www.openwall.com/lists/oss-security/2011/11/15/1 class PasswordTruncateError(PasswordSizeError): """ Error raised if password would be truncated by hash. This derives from :exc:`PasswordSizeError` and :exc:`ValueError`. Hashers such as :class:`~passlib.hash.bcrypt` can be configured to raises this error by setting ``truncate_error=True``. .. attribute:: max_size indicates the maximum allowed size. .. versionadded:: 1.7 """ def __init__(self, cls, msg=None): if msg is None: msg = ("Password too long (%s truncates to %d characters)" % (cls.name, cls.truncate_size)) PasswordSizeError.__init__(self, cls.truncate_size, msg) 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 """ class TokenError(ValueError): """ Base error raised by v:mod:`passlib.totp` when a token can't be parsed / isn't valid / etc. Derives from :exc:`!ValueError`. Usually one of the more specific subclasses below will be raised: * :class:`MalformedTokenError` -- invalid chars, too few digits * :class:`InvalidTokenError` -- no match found * :class:`UsedTokenError` -- match found, but token already used .. versionadded:: 1.7 """ #: default message to use if none provided -- subclasses may fill this in _default_message = 'Token not acceptable' def __init__(self, msg=None, *args, **kwds): if msg is None: msg = self._default_message ValueError.__init__(self, msg, *args, **kwds) class MalformedTokenError(TokenError): """ Error raised by :mod:`passlib.totp` when a token isn't formatted correctly (contains invalid characters, wrong number of digits, etc) """ _default_message = "Unrecognized token" class InvalidTokenError(TokenError): """ Error raised by :mod:`passlib.totp` when a token is formatted correctly, but doesn't match any tokens within valid range. """ _default_message = "Token did not match" class UsedTokenError(TokenError): """ Error raised by :mod:`passlib.totp` if a token is reused. Derives from :exc:`TokenError`. .. autoattribute:: expire_time .. versionadded:: 1.7 """ _default_message = "Token has already been used, please wait for another." #: optional value indicating when current counter period will end, #: and a new token can be generated. expire_time = None def __init__(self, *args, **kwds): self.expire_time = kwds.pop("expire_time", None) TokenError.__init__(self, *args, **kwds) class UnknownHashError(ValueError): """Error raised by :class:`~passlib.crypto.lookup_hash` if hash name is not recognized. This exception derives from :exc:`!ValueError`. .. versionadded:: 1.7 """ def __init__(self, name): self.name = name ValueError.__init__(self, "unknown hash algorithm: %r" % name) #============================================================================= # warnings #============================================================================= class PasslibWarning(UserWarning): """base class for Passlib's user warnings, derives from the builtin :exc:`UserWarning`. .. versionadded:: 1.6 """ # XXX: there's only one reference to this class, and it will go away in 2.0; # so can probably remove this along with this / roll this into PasslibHashWarning. 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) #------------------------------------------------------------------------ # hash/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.7.1/passlib/ext/0000755000175000017500000000000013043774617016554 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/ext/__init__.py0000644000175000017500000000000112214647077020652 0ustar biscuitbiscuit00000000000000 passlib-1.7.1/passlib/ext/django/0000755000175000017500000000000013043774617020016 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/ext/django/__init__.py0000644000175000017500000000034412214647077022126 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.7.1/passlib/ext/django/utils.py0000644000175000017500000013476013015211357021525 0ustar biscuitbiscuit00000000000000"""passlib.ext.django.utils - helper functions used by this plugin""" #============================================================================= # imports #============================================================================= # core from functools import update_wrapper, wraps import logging; log = logging.getLogger(__name__) import sys import weakref 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 import exc, registry from passlib.context import CryptContext from passlib.exc import PasslibRuntimeWarning from passlib.utils.compat import get_method_function, iteritems, OrderedDict, unicode from passlib.utils.decor import memoized_property # local __all__ = [ "DJANGO_VERSION", "MIN_DJANGO_VERSION", "get_preset_config", "get_django_hasher", ] #: minimum version supported by passlib.ext.django MIN_DJANGO_VERSION = (1, 8) #============================================================================= # 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") 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. sha512_crypt__min_rounds = 80000 django_pbkdf2_sha256__min_rounds = 10000 ; set somewhat stronger iteration counts for ``User.is_staff`` staff__sha512_crypt__default_rounds = 100000 staff__django_pbkdf2_sha256__default_rounds = 12500 ; and even stronger ones for ``User.is_superuser`` superuser__sha512_crypt__default_rounds = 120000 superuser__django_pbkdf2_sha256__default_rounds = 15000 """ #============================================================================= # helpers #============================================================================= #: prefix used to shoehorn passlib's handler names into django hasher namespace PASSLIB_WRAPPER_PREFIX = "passlib_" #: prefix used by all the django-specific hash formats in passlib; #: all of these hashes should have a ``.django_name`` attribute. DJANGO_COMPAT_PREFIX = "django_" #: set of hashes w/o "django_" prefix, but which also expose ``.django_name``. _other_django_hashes = set(["hex_md5"]) def _wrap_method(method): """wrap method object in bare function""" @wraps(method) def wrapper(*args, **kwds): return method(*args, **kwds) return wrapper #============================================================================= # translator #============================================================================= class DjangoTranslator(object): """ Object which helps translate passlib hasher objects / names to and from django hasher objects / names. These methods are wrapped in a class so that results can be cached, but with the ability to have independant caches, since django hasher names may / may not correspond to the same instance (or even class). """ #============================================================================= # instance attrs #============================================================================= #: CryptContext instance #: (if any -- generally only set by DjangoContextAdapter subclass) context = None #: internal cache of passlib hasher -> django hasher instance. #: key stores weakref to passlib hasher. _django_hasher_cache = None #: special case -- unsalted_sha1 _django_unsalted_sha1 = None #: internal cache of django name -> passlib hasher #: value stores weakrefs to passlib hasher. _passlib_hasher_cache = None #============================================================================= # init #============================================================================= def __init__(self, context=None, **kwds): super(DjangoTranslator, self).__init__(**kwds) if context is not None: self.context = context self._django_hasher_cache = weakref.WeakKeyDictionary() self._passlib_hasher_cache = weakref.WeakValueDictionary() def reset_hashers(self): self._django_hasher_cache.clear() self._passlib_hasher_cache.clear() self._django_unsalted_sha1 = None def _get_passlib_hasher(self, passlib_name): """ resolve passlib hasher by name, using context if available. """ context = self.context if context is None: return registry.get_crypt_handler(passlib_name) else: return context.handler(passlib_name) #============================================================================= # resolve passlib hasher -> django hasher #============================================================================= def passlib_to_django_name(self, passlib_name): """ Convert passlib hasher / name to Django hasher name. """ return self.passlib_to_django(passlib_name).algorithm # XXX: add option (in class, or call signature) to always return a wrapper, # rather than native builtin -- would let HashersTest check that # our own wrapper + implementations are matching up with their tests. def passlib_to_django(self, passlib_hasher, cached=True): """ Convert passlib hasher / name to Django hasher. :param passlib_hasher: passlib hasher / name :returns: django hasher instance """ # resolve names to hasher if not hasattr(passlib_hasher, "name"): passlib_hasher = self._get_passlib_hasher(passlib_hasher) # check cache if cached: cache = self._django_hasher_cache try: return cache[passlib_hasher] except KeyError: pass result = cache[passlib_hasher] = \ self.passlib_to_django(passlib_hasher, cached=False) return result # find native equivalent, and return wrapper if there isn't one django_name = getattr(passlib_hasher, "django_name", None) if django_name: return self._create_django_hasher(django_name) else: return _PasslibHasherWrapper(passlib_hasher) _builtin_django_hashers = dict( md5="MD5PasswordHasher", ) def _create_django_hasher(self, django_name): """ helper to create new django hasher by name. wraps underlying django methods. """ # if we haven't patched django, can use it directly module = sys.modules.get("passlib.ext.django.models") if module is None or not module.adapter.patched: from django.contrib.auth.hashers import get_hasher return get_hasher(django_name) # We've patched django's get_hashers(), so calling django's get_hasher() # or get_hashers_by_algorithm() would only land us back here. # As non-ideal workaround, have to use original get_hashers(), get_hashers = module.adapter._manager.getorig("django.contrib.auth.hashers:get_hashers").__wrapped__ for hasher in get_hashers(): if hasher.algorithm == django_name: return hasher # hardcode a few for cases where get_hashers() look won't work. path = self._builtin_django_hashers.get(django_name) if path: if "." not in path: path = "django.contrib.auth.hashers." + path from django.utils.module_loading import import_string return import_string(path)() raise ValueError("unknown hasher: %r" % django_name) #============================================================================= # reverse django -> passlib #============================================================================= def django_to_passlib_name(self, django_name): """ Convert Django hasher / name to Passlib hasher name. """ return self.django_to_passlib(django_name).name def django_to_passlib(self, django_name, cached=True): """ Convert Django hasher / name to Passlib hasher / name. If present, CryptContext will be checked instead of main registry. :param django_name: Django hasher class or algorithm name. "default" allowed if context provided. :raises ValueError: if can't resolve hasher. :returns: passlib hasher or name """ # check for django hasher if hasattr(django_name, "algorithm"): # check for passlib adapter if isinstance(django_name, _PasslibHasherWrapper): return django_name.passlib_handler # resolve django hasher -> name django_name = django_name.algorithm # check cache if cached: cache = self._passlib_hasher_cache try: return cache[django_name] except KeyError: pass result = cache[django_name] = \ self.django_to_passlib(django_name, cached=False) return result # check if it's an obviously-wrapped name if django_name.startswith(PASSLIB_WRAPPER_PREFIX): passlib_name = django_name[len(PASSLIB_WRAPPER_PREFIX):] return self._get_passlib_hasher(passlib_name) # resolve default if django_name == "default": context = self.context if context is None: raise TypeError("can't determine default scheme w/ context") return context.handler() # special case: Django uses a separate hasher for "sha1$$digest" # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1); # but passlib uses "django_salted_sha1" for both of these. if django_name == "unsalted_sha1": django_name = "sha1" # resolve name # XXX: bother caching these lists / mapping? # not needed in long-term due to cache above. context = self.context if context is None: # check registry # TODO: should make iteration via registry easier candidates = ( registry.get_crypt_handler(passlib_name) for passlib_name in registry.list_crypt_handlers() if passlib_name.startswith(DJANGO_COMPAT_PREFIX) or passlib_name in _other_django_hashes ) else: # check context candidates = context.schemes(resolve=True) for handler in candidates: if getattr(handler, "django_name", None) == django_name: return handler # give up # NOTE: this should only happen for custom django hashers that we don't # know the equivalents for. _HasherHandler (below) is work in # progress that would allow us to at least return a wrapper. raise ValueError("can't translate django name to passlib name: %r" % (django_name,)) #============================================================================= # django hasher lookup #============================================================================= def resolve_django_hasher(self, django_name, cached=True): """ Take in a django algorithm name, return django hasher. """ # check for django hasher if hasattr(django_name, "algorithm"): return django_name # resolve to passlib hasher passlib_hasher = self.django_to_passlib(django_name, cached=cached) # special case: Django uses a separate hasher for "sha1$$digest" # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1); # but passlib uses "django_salted_sha1" for both of these. # XXX: this isn't ideal way to handle this. would like to do something # like pass "django_variant=django_name" into passlib_to_django(), # and have it cache separate hasher there. # but that creates a LOT of complication in it's cache structure, # for what is just one special case. if django_name == "unsalted_sha1" and passlib_hasher.name == "django_salted_sha1": if not cached: return self._create_django_hasher(django_name) result = self._django_unsalted_sha1 if result is None: result = self._django_unsalted_sha1 = self._create_django_hasher(django_name) return result # lookup corresponding django hasher return self.passlib_to_django(passlib_hasher, cached=cached) #============================================================================= # eoc #============================================================================= #============================================================================= # adapter #============================================================================= class DjangoContextAdapter(DjangoTranslator): """ Object which tries to adapt a Passlib CryptContext object, using a Django-hasher compatible API. When installed in django, :mod:`!passlib.ext.django` will create an instance of this class, and then monkeypatch the appropriate methods into :mod:`!django.contrib.auth` and other appropriate places. """ #============================================================================= # instance attrs #============================================================================= #: CryptContext instance we're wrapping context = None #: ref to original make_password(), #: needed to generate usuable passwords that match django _orig_make_password = None #: ref to django helper of this name -- not monkeypatched is_password_usable = None #: PatchManager instance used to track installation _manager = None #: whether config=disabled flag was set enabled = True #: patch status patched = False #============================================================================= # init #============================================================================= def __init__(self, context=None, get_user_category=None, **kwds): # init log self.log = logging.getLogger(__name__ + ".DjangoContextAdapter") # init parent, filling in default context object if context is None: context = CryptContext() super(DjangoContextAdapter, self).__init__(context=context, **kwds) # setup user category if get_user_category: assert callable(get_user_category) self.get_user_category = get_user_category # install lru cache wrappers from django.utils.lru_cache import lru_cache self.get_hashers = lru_cache()(self.get_hashers) # get copy of original make_password from django.contrib.auth.hashers import make_password if make_password.__module__.startswith("passlib."): make_password = _PatchManager.peek_unpatched_func(make_password) self._orig_make_password = make_password # get other django helpers from django.contrib.auth.hashers import is_password_usable self.is_password_usable = is_password_usable # init manager mlog = logging.getLogger(__name__ + ".DjangoContextAdapter._manager") self._manager = _PatchManager(log=mlog) def reset_hashers(self): """ Wrapper to manually reset django's hasher lookup cache """ # resets cache for .get_hashers() & .get_hashers_by_algorithm() from django.contrib.auth.hashers import reset_hashers reset_hashers(setting="PASSWORD_HASHERS") # reset internal caches super(DjangoContextAdapter, self).reset_hashers() #============================================================================= # django hashers helpers -- hasher lookup #============================================================================= # lru_cache()'ed by init def get_hashers(self): """ Passlib replacement for get_hashers() -- Return list of available django hasher classes """ passlib_to_django = self.passlib_to_django return [passlib_to_django(hasher) for hasher in self.context.schemes(resolve=True)] def get_hasher(self, algorithm="default"): """ Passlib replacement for get_hasher() -- Return django hasher by name """ return self.resolve_django_hasher(algorithm) def identify_hasher(self, encoded): """ Passlib replacement for identify_hasher() -- Identify django hasher based on hash. """ handler = self.context.identify(encoded, resolve=True, required=True) if handler.name == "django_salted_sha1" and encoded.startswith("sha1$$"): # Django uses a separate hasher for "sha1$$digest" hashes, but # passlib identifies it as belonging to "sha1$salt$digest" handler. # We want to resolve to correct django hasher. return self.get_hasher("unsalted_sha1") return self.passlib_to_django(handler) #============================================================================= # django.contrib.auth.hashers helpers -- password helpers #============================================================================= def make_password(self, password, salt=None, hasher="default"): """ Passlib replacement for make_password() """ if password is None: return self._orig_make_password(None) # NOTE: relying on hasher coming from context, and thus having # context-specific config baked into it. passlib_hasher = self.django_to_passlib(hasher) if "salt" not in passlib_hasher.setting_kwds: # ignore salt param even if preset pass elif hasher.startswith("unsalted_"): # Django 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. passlib_hasher = passlib_hasher.using(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. passlib_hasher = passlib_hasher.using(salt=salt) return passlib_hasher.hash(password) def check_password(self, 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 password is None or not self.is_password_usable(encoded): return False # verify password context = self.context correct = context.verify(password, encoded) if not (correct and setter): return correct # check if we need to rehash if preferred == "default": if not context.needs_update(encoded, secret=password): return correct else: # Django's check_password() won't call setter() on a # 'preferred' alg, even if it's otherwise deprecated. To try and # replicate this behavior if preferred is set, we look up the # passlib hasher, and call it's original needs_update() method. # TODO: Solve redundancy that verify() call # above is already identifying hash. hasher = self.django_to_passlib(preferred) if (hasher.identify(encoded) and not hasher.needs_update(encoded, secret=password)): # alg is 'preferred' and hash itself doesn't need updating, # so nothing to do. return correct # else: either hash isn't preferred, or it needs updating. # call setter to rehash setter(password) return correct #============================================================================= # django users helpers #============================================================================= def user_check_password(self, user, password): """ Passlib replacement for User.check_password() """ if password is None: return False hash = user.password if not self.is_password_usable(hash): return False cat = self.get_user_category(user) ok, new_hash = self.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 def user_set_password(self, user, password): """ Passlib replacement for User.set_password() """ if password is None: user.set_unusable_password() else: cat = self.get_user_category(user) user.password = self.context.hash(password, category=cat) def get_user_category(self, user): """ Helper for hashing passwords per-user -- figure out the CryptContext category for specified Django user object. .. note:: This may be overridden via PASSLIB_GET_CATEGORY django setting """ if user.is_superuser: return "superuser" elif user.is_staff: return "staff" else: return None #============================================================================= # patch control #============================================================================= HASHERS_PATH = "django.contrib.auth.hashers" MODELS_PATH = "django.contrib.auth.models" USER_CLASS_PATH = MODELS_PATH + ":User" FORMS_PATH = "django.contrib.auth.forms" #: list of locations to patch patch_locations = [ # # User object # NOTE: could leave defaults alone, but want to have user available # so that we can support get_user_category() # (USER_CLASS_PATH + ".check_password", "user_check_password", dict(method=True)), (USER_CLASS_PATH + ".set_password", "user_set_password", dict(method=True)), # # Hashers module # (HASHERS_PATH + ":", "check_password"), (HASHERS_PATH + ":", "make_password"), (HASHERS_PATH + ":", "get_hashers"), (HASHERS_PATH + ":", "get_hasher"), (HASHERS_PATH + ":", "identify_hasher"), # # Patch known imports from hashers module # (MODELS_PATH + ":", "check_password"), (MODELS_PATH + ":", "make_password"), (FORMS_PATH + ":", "get_hasher"), (FORMS_PATH + ":", "identify_hasher"), ] def install_patch(self): """ Install monkeypatch to replace django hasher framework. """ # don't reapply log = self.log if self.patched: log.warning("monkeypatching already applied, refusing to reapply") return False # version check if DJANGO_VERSION < MIN_DJANGO_VERSION: raise RuntimeError("passlib.ext.django requires django >= %s" % (MIN_DJANGO_VERSION,)) # log start log.debug("preparing to monkeypatch django ...") # run through patch locations manager = self._manager for record in self.patch_locations: if len(record) == 2: record += ({},) target, source, opts = record if target.endswith((":", ",")): target += source value = getattr(self, source) if opts.get("method"): # have to wrap our method in a function, # since we're installing it in a class *as* a method # XXX: make this a flag for .patch()? value = _wrap_method(value) manager.patch(target, value) # reset django's caches (e.g. get_hash_by_algorithm) self.reset_hashers() # done! self.patched = True log.debug("... finished monkeypatching django") return True def remove_patch(self): """ Remove monkeypatch from django hasher framework. As precaution in case there are lingering refs to context, context object will be wiped. .. 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. """ log = self.log manager = self._manager if self.patched: log.debug("removing django monkeypatching...") manager.unpatch_all(unpatch_conflicts=True) self.context.load({}) self.patched = False self.reset_hashers() log.debug("...finished removing django monkeypatching") return True if manager.isactive(): # pragma: no cover -- sanity check log.warning("reverting partial monkeypatching of django...") manager.unpatch_all() self.context.load({}) self.reset_hashers() log.debug("...finished removing django monkeypatching") return True log.debug("django not monkeypatched") return False #============================================================================= # loading config #============================================================================= def load_model(self): """ Load configuration from django, and install patch. """ self._load_settings() if self.enabled: try: self.install_patch() except: # try to undo what we can self.remove_patch() raise else: if self.patched: # pragma: no cover -- sanity check log.error("didn't expect monkeypatching would be applied!") self.remove_patch() log.debug("passlib.ext.django loaded") def _load_settings(self): """ Update settings from django """ from django.conf import settings # 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 exc.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 exc.ExpectedTypeError(get_category, "callable", "PASSLIB_GET_CATEGORY") # check if we've been disabled if config == "disabled": self.enabled = False return else: self.__dict__.pop("enabled", None) # resolve any preset aliases if isinstance(config, str) and '\n' not in config: config = get_preset_config(config) # setup category func if get_category: self.get_user_category = get_category else: self.__dict__.pop("get_category", None) # setup context self.context.load(config) self.reset_hashers() #============================================================================= # eof #============================================================================= #============================================================================= # wrapping passlib handlers as django hashers #============================================================================= _GEN_SALT_SIGNAL = "--!!!generate-new-salt!!!--" class ProxyProperty(object): """helper that proxies another attribute""" def __init__(self, attr): self.attr = attr def __get__(self, obj, cls): if obj is None: cls = obj return getattr(obj, self.attr) def __set__(self, obj, value): setattr(obj, self.attr, value) def __delete__(self, obj): delattr(obj, self.attr) class _PasslibHasherWrapper(object): """ adapter which which wraps a :cls:`passlib.ifc.PasswordHash` class, and provides an interface compatible with the Django hasher API. :param passlib_handler: passlib hash handler (e.g. :cls:`passlib.hash.sha256_crypt`. """ #===================================================================== # instance attrs #===================================================================== #: passlib handler that we're adapting. passlib_handler = None # NOTE: 'rounds' attr will store variable rounds, IF handler supports it. # 'iterations' will act as proxy, for compatibility with django pbkdf2 hashers. # rounds = None # iterations = None #===================================================================== # init #===================================================================== def __init__(self, passlib_handler): # init handler if getattr(passlib_handler, "django_name", None): raise ValueError("handlers that reflect an official django " "hasher shouldn't be wrapped: %r" % (passlib_handler.name,)) if passlib_handler.is_disabled: # XXX: could this be implemented? raise ValueError("can't wrap disabled-hash handlers: %r" % (passlib_handler.name)) self.passlib_handler = passlib_handler # init rounds support if self._has_rounds: self.rounds = passlib_handler.default_rounds self.iterations = ProxyProperty("rounds") #===================================================================== # internal methods #===================================================================== def __repr__(self): return "" % self.passlib_handler #===================================================================== # internal properties #===================================================================== @memoized_property def __name__(self): return "Passlib_%s_PasswordHasher" % self.passlib_handler.name.title() @memoized_property def _has_rounds(self): return "rounds" in self.passlib_handler.setting_kwds @memoized_property def _translate_kwds(self): """ internal helper for safe_summary() -- used to translate passlib hash options -> django keywords """ out = dict(checksum="hash") if self._has_rounds and "pbkdf2" in self.passlib_handler.name: out['rounds'] = 'iterations' return out #===================================================================== # hasher properties #===================================================================== @memoized_property def algorithm(self): return PASSLIB_WRAPPER_PREFIX + self.passlib_handler.name #===================================================================== # hasher api #===================================================================== def salt(self): # NOTE: passlib's handler.hash() 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, rounds=None, iterations=None): kwds = {} if salt is not None and salt != _GEN_SALT_SIGNAL: kwds['salt'] = salt if self._has_rounds: if rounds is not None: kwds['rounds'] = rounds elif iterations is not None: kwds['rounds'] = iterations else: kwds['rounds'] = self.rounds elif rounds is not None or iterations is not None: warn("%s.hash(): 'rounds' and 'iterations' are ignored" % self.__name__) handler = self.passlib_handler if kwds: handler = handler.using(**kwds) return handler.hash(password) def safe_summary(self, encoded): from django.contrib.auth.hashers import mask_hash from django.utils.translation import ugettext_noop as _ 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 OrderedDict(items) def must_update(self, encoded): # TODO: would like access CryptContext, would need caller to pass it to get_passlib_hasher(). # for now (as of passlib 1.6.6), replicating django policy that this returns True # if 'encoded' hash has different rounds value from self.rounds if self._has_rounds: # XXX: could cache this subclass somehow (would have to intercept writes to self.rounds) # TODO: always call subcls/handler.needs_update() in case there's other things to check subcls = self.passlib_handler.using(min_rounds=self.rounds, max_rounds=self.rounds) if subcls.needs_update(encoded): return True return False #===================================================================== # eoc #===================================================================== #============================================================================= # 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 hash(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 = {} def isactive(self): return bool(self._state) # bool value tests if any patches are currently applied. # NOTE: this behavior is deprecated in favor of .isactive __bool__ = __nonzero__ = isactive 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, wrap=False): """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) if wrap: assert callable(value) wrapped = orig wrapped_by = value def wrapper(*args, **kwds): return wrapped_by(wrapped, *args, **kwds) update_wrapper(wrapper, value) value = wrapper if callable(value): # needed by DjangoContextAdapter init get_method_function(value)._patched_original_value = orig self._set_path(path, value) self._state[path] = (orig, value) @classmethod def peek_unpatched_func(cls, value): return value._patched_original_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, wrap=False): """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, wrap=wrap) return func if callable(name): # called in non-decorator mode func = name name = None builder(func) return None 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.7.1/passlib/ext/django/models.py0000644000175000017500000000244213015205366021642 0ustar biscuitbiscuit00000000000000"""passlib.ext.django.models -- monkeypatch django hashing framework""" #============================================================================= # imports #============================================================================= # core # site # pkg from passlib.context import CryptContext from passlib.ext.django.utils import DjangoContextAdapter # local __all__ = ["password_context"] #============================================================================= # global attrs #============================================================================= #: adapter instance used to drive most of this adapter = DjangoContextAdapter() # the context object which this patches contrib.auth to use for password hashing. # configuration controlled by ``settings.PASSLIB_CONFIG``. password_context = adapter.context #: hook callers should use if context is changed context_changed = adapter.reset_hashers #============================================================================= # main code #============================================================================= # load config & install monkeypatch adapter.load_model() #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/crypto/0000755000175000017500000000000013043774617017274 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/crypto/__init__.py0000644000175000017500000000012413015205366021367 0ustar biscuitbiscuit00000000000000"""passlib.crypto -- package containing cryptographic primitives used by passlib""" passlib-1.7.1/passlib/crypto/_blowfish/0000755000175000017500000000000013043774617021250 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/crypto/_blowfish/__init__.py0000644000175000017500000001443213015205366023352 0ustar biscuitbiscuit00000000000000"""passlib.crypto._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 getrandbytes, rng from passlib.utils.binary import bcrypt64 from passlib.utils.compat import BytesIO, unicode, u, native_string_types from passlib.crypto._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 log_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.7.1/passlib/crypto/_blowfish/unrolled.py0000644000175000017500000011044113015205366023434 0ustar biscuitbiscuit00000000000000"""passlib.crypto._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.crypto._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.7.1/passlib/crypto/_blowfish/_gen_files.py0000644000175000017500000001404013015205366023700 0ustar biscuitbiscuit00000000000000"""passlib.crypto._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.crypto._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.crypto._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.7.1/passlib/crypto/scrypt/0000755000175000017500000000000013043774617020620 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/crypto/scrypt/__init__.py0000644000175000017500000001530613041425350022717 0ustar biscuitbiscuit00000000000000"""passlib.utils.scrypt -- scrypt hash frontend and help utilities""" #========================================================================== # imports #========================================================================== from __future__ import absolute_import # core import logging; log = logging.getLogger(__name__) from warnings import warn # pkg from passlib import exc from passlib.utils import to_bytes from passlib.utils.compat import PYPY # local __all__ =[ "validate", "scrypt", ] #========================================================================== # config validation #========================================================================== #: max output length in bytes MAX_KEYLEN = ((1 << 32) - 1) * 32 #: max ``r * p`` limit MAX_RP = (1 << 30) - 1 # TODO: unittests for this function def validate(n, r, p): """ helper which validates a set of scrypt config parameters. scrypt will take ``O(n * r * p)`` time and ``O(n * r)`` memory. limitations are that ``n = 2**``, ``n < 2**(16*r)``, ``r * p < 2 ** 30``. :param n: scrypt rounds :param r: scrypt block size :param p: scrypt parallel factor """ if r < 1: raise ValueError("r must be > 0: r=%r" % r) if p < 1: raise ValueError("p must be > 0: p=%r" % p) if r * p > MAX_RP: # pbkdf2-hmac-sha256 limitation - it will be requested to generate ``p*(2*r)*64`` bytes, # but pbkdf2 can do max of (2**31-1) blocks, and sha-256 has 32 byte block size... # so ``(2**31-1)*32 >= p*r*128`` -> ``r*p < 2**30`` raise ValueError("r * p must be < 2**30: r=%r, p=%r" % (r,p)) if n < 2 or n & (n - 1): raise ValueError("n must be > 1, and a power of 2: n=%r" % n) return True # TODO: configuration picker (may need psutil for full effect) #========================================================================== # hash frontend #========================================================================== #: backend function used by scrypt(), filled in by _set_backend() _scrypt = None #: name of backend currently in use, exposed for informational purposes. backend = None def scrypt(secret, salt, n, r, p=1, keylen=32): """run SCrypt key derivation function using specified parameters. :arg secret: passphrase string (unicode is encoded to bytes using utf-8). :arg salt: salt string (unicode is encoded to bytes using utf-8). :arg n: integer 'N' parameter :arg r: integer 'r' parameter :arg p: integer 'p' parameter :arg keylen: number of bytes of key to generate. defaults to 32 (the internal block size). :returns: a *keylen*-sized bytes instance SCrypt imposes a number of constraints on it's input parameters: * ``r * p < 2**30`` -- due to a limitation of PBKDF2-HMAC-SHA256. * ``keylen < (2**32 - 1) * 32`` -- due to a limitation of PBKDF2-HMAC-SHA256. * ``n`` must a be a power of 2, and > 1 -- internal limitation of scrypt() implementation :raises ValueError: if the provided parameters are invalid (see constraints above). .. warning:: Unless the third-party ``scrypt ``_ package is installed, passlib will use a builtin pure-python implementation of scrypt, which is *considerably* slower (and thus requires a much lower / less secure ``n`` value in order to be usuable). Installing the :mod:`!scrypt` package is strongly recommended. """ validate(n, r, p) secret = to_bytes(secret, param="secret") salt = to_bytes(salt, param="salt") if keylen < 1: raise ValueError("keylen must be at least 1") if keylen > MAX_KEYLEN: raise ValueError("keylen too large, must be <= %d" % MAX_KEYLEN) return _scrypt(secret, salt, n, r, p, keylen) def _load_builtin_backend(): """ Load pure-python scrypt implementation built into passlib. """ slowdown = 10 if PYPY else 100 warn("Using builtin scrypt backend, which is %dx slower than is required " "for adequate security. Installing scrypt support (via 'pip install scrypt') " "is strongly recommended" % slowdown, exc.PasslibSecurityWarning) from ._builtin import ScryptEngine return ScryptEngine.execute def _load_cffi_backend(): """ Try to import the ctypes-based scrypt hash function provided by the ``scrypt ``_ package. """ try: from scrypt import hash return hash except ImportError: pass # not available, but check to see if package present but outdated / not installed right try: import scrypt except ImportError as err: if "scrypt" not in str(err): # e.g. if cffi isn't set up right # user should try importing scrypt explicitly to diagnose problem. warn("'scrypt' package failed to import correctly (possible installation issue?)", exc.PasslibWarning) # else: package just isn't installed else: warn("'scrypt' package is too old (lacks ``hash()`` method)", exc.PasslibWarning) return None #: list of potential backends backend_values = ("scrypt", "builtin") #: dict mapping backend name -> loader _backend_loaders = dict( scrypt=_load_cffi_backend, # XXX: rename backend constant to "cffi"? builtin=_load_builtin_backend, ) def _set_backend(name, dryrun=False): """ set backend for scrypt(). if name not specified, loads first available. :raises ~passlib.exc.MissingBackendError: if backend can't be found .. note:: mainly intended to be called by unittests, and scrypt hash handler """ if name == "any": return elif name == "default": for name in backend_values: try: return _set_backend(name, dryrun=dryrun) except exc.MissingBackendError: continue raise exc.MissingBackendError("no scrypt backends available") else: loader = _backend_loaders.get(name) if not loader: raise ValueError("unknown scrypt backend: %r" % (name,)) hash = loader() if not hash: raise exc.MissingBackendError("scrypt backend %r not available" % name) if dryrun: return global _scrypt, backend backend = name _scrypt = hash # initialize backend _set_backend("default") def _has_backend(name): try: _set_backend(name, dryrun=True) return True except exc.MissingBackendError: return False #========================================================================== # eof #========================================================================== passlib-1.7.1/passlib/crypto/scrypt/_salsa.py0000644000175000017500000001312713015205366022425 0ustar biscuitbiscuit00000000000000"""passlib.utils.scrypt._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" #================================================================= # salsa function #================================================================= def salsa20(input): """apply the salsa20/8 core to the provided input :args input: input list containing 16 32-bit integers :returns: result list containing 16 32-bit integers """ b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 = input v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15 = \ b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 i = 0 while i < 4: # salsa op 0: [4] ^= ([0]+[12])<<<7 t = (v0 + v12) & 0xffffffff v4 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 1: [8] ^= ([4]+[0])<<<9 t = (v4 + v0) & 0xffffffff v8 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 2: [12] ^= ([8]+[4])<<<13 t = (v8 + v4) & 0xffffffff v12 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 3: [0] ^= ([12]+[8])<<<18 t = (v12 + v8) & 0xffffffff v0 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 4: [9] ^= ([5]+[1])<<<7 t = (v5 + v1) & 0xffffffff v9 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 5: [13] ^= ([9]+[5])<<<9 t = (v9 + v5) & 0xffffffff v13 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 6: [1] ^= ([13]+[9])<<<13 t = (v13 + v9) & 0xffffffff v1 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 7: [5] ^= ([1]+[13])<<<18 t = (v1 + v13) & 0xffffffff v5 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 8: [14] ^= ([10]+[6])<<<7 t = (v10 + v6) & 0xffffffff v14 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 9: [2] ^= ([14]+[10])<<<9 t = (v14 + v10) & 0xffffffff v2 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 10: [6] ^= ([2]+[14])<<<13 t = (v2 + v14) & 0xffffffff v6 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 11: [10] ^= ([6]+[2])<<<18 t = (v6 + v2) & 0xffffffff v10 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 12: [3] ^= ([15]+[11])<<<7 t = (v15 + v11) & 0xffffffff v3 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 13: [7] ^= ([3]+[15])<<<9 t = (v3 + v15) & 0xffffffff v7 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 14: [11] ^= ([7]+[3])<<<13 t = (v7 + v3) & 0xffffffff v11 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 15: [15] ^= ([11]+[7])<<<18 t = (v11 + v7) & 0xffffffff v15 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 16: [1] ^= ([0]+[3])<<<7 t = (v0 + v3) & 0xffffffff v1 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 17: [2] ^= ([1]+[0])<<<9 t = (v1 + v0) & 0xffffffff v2 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 18: [3] ^= ([2]+[1])<<<13 t = (v2 + v1) & 0xffffffff v3 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 19: [0] ^= ([3]+[2])<<<18 t = (v3 + v2) & 0xffffffff v0 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 20: [6] ^= ([5]+[4])<<<7 t = (v5 + v4) & 0xffffffff v6 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 21: [7] ^= ([6]+[5])<<<9 t = (v6 + v5) & 0xffffffff v7 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 22: [4] ^= ([7]+[6])<<<13 t = (v7 + v6) & 0xffffffff v4 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 23: [5] ^= ([4]+[7])<<<18 t = (v4 + v7) & 0xffffffff v5 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 24: [11] ^= ([10]+[9])<<<7 t = (v10 + v9) & 0xffffffff v11 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 25: [8] ^= ([11]+[10])<<<9 t = (v11 + v10) & 0xffffffff v8 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 26: [9] ^= ([8]+[11])<<<13 t = (v8 + v11) & 0xffffffff v9 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 27: [10] ^= ([9]+[8])<<<18 t = (v9 + v8) & 0xffffffff v10 ^= ((t & 0x00003fff) << 18) | (t >> 14) # salsa op 28: [12] ^= ([15]+[14])<<<7 t = (v15 + v14) & 0xffffffff v12 ^= ((t & 0x01ffffff) << 7) | (t >> 25) # salsa op 29: [13] ^= ([12]+[15])<<<9 t = (v12 + v15) & 0xffffffff v13 ^= ((t & 0x007fffff) << 9) | (t >> 23) # salsa op 30: [14] ^= ([13]+[12])<<<13 t = (v13 + v12) & 0xffffffff v14 ^= ((t & 0x0007ffff) << 13) | (t >> 19) # salsa op 31: [15] ^= ([14]+[13])<<<18 t = (v14 + v13) & 0xffffffff v15 ^= ((t & 0x00003fff) << 18) | (t >> 14) i += 1 b0 = (b0 + v0) & 0xffffffff b1 = (b1 + v1) & 0xffffffff b2 = (b2 + v2) & 0xffffffff b3 = (b3 + v3) & 0xffffffff b4 = (b4 + v4) & 0xffffffff b5 = (b5 + v5) & 0xffffffff b6 = (b6 + v6) & 0xffffffff b7 = (b7 + v7) & 0xffffffff b8 = (b8 + v8) & 0xffffffff b9 = (b9 + v9) & 0xffffffff b10 = (b10 + v10) & 0xffffffff b11 = (b11 + v11) & 0xffffffff b12 = (b12 + v12) & 0xffffffff b13 = (b13 + v13) & 0xffffffff b14 = (b14 + v14) & 0xffffffff b15 = (b15 + v15) & 0xffffffff return b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 #================================================================= # eof #================================================================= passlib-1.7.1/passlib/crypto/scrypt/_builtin.py0000644000175000017500000002131613015205366022767 0ustar biscuitbiscuit00000000000000"""passlib.utils.scrypt._builtin -- scrypt() kdf in pure-python""" #========================================================================== # imports #========================================================================== # core import operator import struct # pkg from passlib.utils.compat import izip from passlib.crypto.digest import pbkdf2_hmac from passlib.crypto.scrypt._salsa import salsa20 # local __all__ =[ "ScryptEngine", ] #========================================================================== # scrypt engine #========================================================================== class ScryptEngine(object): """ helper class used to run scrypt kdf, see scrypt() for frontend .. warning:: this class does NO validation of the input ranges or types. it's not intended to be used directly, but only as a backend for :func:`passlib.utils.scrypt.scrypt()`. """ #================================================================= # instance attrs #================================================================= # primary scrypt config parameters n = 0 r = 0 p = 0 # derived values & objects smix_bytes = 0 iv_bytes = 0 bmix_len = 0 bmix_half_len = 0 bmix_struct = None integerify = None #================================================================= # frontend #================================================================= @classmethod def execute(cls, secret, salt, n, r, p, keylen): """create engine & run scrypt() hash calculation""" return cls(n, r, p).run(secret, salt, keylen) #================================================================= # init #================================================================= def __init__(self, n, r, p): # store config self.n = n self.r = r self.p = p self.smix_bytes = r << 7 # num bytes in smix input - 2*r*16*4 self.iv_bytes = self.smix_bytes * p self.bmix_len = bmix_len = r << 5 # length of bmix block list - 32*r integers self.bmix_half_len = r << 4 assert struct.calcsize("I") == 4 self.bmix_struct = struct.Struct("<" + str(bmix_len) + "I") # use optimized bmix for certain cases if r == 1: self.bmix = self._bmix_1 # pick best integerify function - integerify(bmix_block) should # take last 64 bytes of block and return a little-endian integer. # since it's immediately converted % n, we only have to extract # the first 32 bytes if n < 2**32 - which due to the current # internal representation, is already unpacked as a 32-bit int. if n <= 0xFFFFffff: integerify = operator.itemgetter(-16) else: assert n <= 0xFFFFffffFFFFffff ig1 = operator.itemgetter(-16) ig2 = operator.itemgetter(-17) def integerify(X): return ig1(X) | (ig2(X)<<32) self.integerify = integerify #================================================================= # frontend #================================================================= def run(self, secret, salt, keylen): """ run scrypt kdf for specified secret, salt, and keylen .. note:: * time cost is ``O(n * r * p)`` * mem cost is ``O(n * r)`` """ # stretch salt into initial byte array via pbkdf2 iv_bytes = self.iv_bytes input = pbkdf2_hmac("sha256", secret, salt, rounds=1, keylen=iv_bytes) # split initial byte array into 'p' mflen-sized chunks, # and run each chunk through smix() to generate output chunk. smix = self.smix if self.p == 1: output = smix(input) else: # XXX: *could* use threading here, if really high p values encountered, # but would tradeoff for more memory usage. smix_bytes = self.smix_bytes output = b''.join( smix(input[offset:offset+smix_bytes]) for offset in range(0, iv_bytes, smix_bytes) ) # stretch final byte array into output via pbkdf2 return pbkdf2_hmac("sha256", secret, output, rounds=1, keylen=keylen) #================================================================= # smix() helper #================================================================= def smix(self, input): """run SCrypt smix function on a single input block :arg input: byte string containing input data. interpreted as 32*r little endian 4 byte integers. :returns: byte string containing output data derived by mixing input using n & r parameters. .. note:: time & mem cost are both ``O(n * r)`` """ # gather locals bmix = self.bmix bmix_struct = self.bmix_struct integerify = self.integerify n = self.n # parse input into 32*r integers ('X' in scrypt source) # mem cost -- O(r) buffer = list(bmix_struct.unpack(input)) # starting with initial buffer contents, derive V s.t. # V[0]=initial_buffer ... V[i] = bmix(V[i-1], V[i-1]) ... V[n-1] = bmix(V[n-2], V[n-2]) # final buffer contents should equal bmix(V[n-1], V[n-1]) # # time cost -- O(n * r) -- n loops, bmix is O(r) # mem cost -- O(n * r) -- V is n-element array of r-element tuples # NOTE: could do time / memory tradeoff to shrink size of V def vgen(): i = 0 while i < n: last = tuple(buffer) yield last bmix(last, buffer) i += 1 V = list(vgen()) # generate result from X & V. # # time cost -- O(n * r) -- loops n times, calls bmix() which has O(r) time cost # mem cost -- O(1) -- allocates nothing, calls bmix() which has O(1) mem cost get_v_elem = V.__getitem__ n_mask = n - 1 i = 0 while i < n: j = integerify(buffer) & n_mask result = tuple(a ^ b for a, b in izip(buffer, get_v_elem(j))) bmix(result, buffer) i += 1 # # NOTE: we could easily support arbitrary values of ``n``, not just powers of 2, # # but very few implementations have that ability, so not enabling it for now... # if not n_is_log_2: # while i < n: # j = integerify(buffer) % n # tmp = tuple(a^b for a,b in izip(buffer, get_v_elem(j))) # bmix(tmp,buffer) # i += 1 # repack tmp return bmix_struct.pack(*buffer) #================================================================= # bmix() helper #================================================================= def bmix(self, source, target): """ block mixing function used by smix() uses salsa20/8 core to mix block contents. :arg source: source to read from. should be list of 32*r 4-byte integers (2*r salsa20 blocks). :arg target: target to write to. should be list with same size as source. the existing value of this buffer is ignored. .. warning:: this operates *in place* on target, so source & target should NOT be same list. .. note:: * time cost is ``O(r)`` -- loops 16*r times, salsa20() has ``O(1)`` cost. * memory cost is ``O(1)`` -- salsa20() uses 16 x uint4, all other operations done in-place. """ ## assert source is not target # Y[-1] = B[2r-1], Y[i] = hash( Y[i-1] xor B[i]) # B' <-- (Y_0, Y_2 ... Y_{2r-2}, Y_1, Y_3 ... Y_{2r-1}) */ half = self.bmix_half_len # 16*r out of 32*r - start of Y_1 tmp = source[-16:] # 'X' in scrypt source siter = iter(source) j = 0 while j < half: jn = j+16 target[j:jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter)) target[half+j:half+jn] = tmp = salsa20(a ^ b for a, b in izip(tmp, siter)) j = jn def _bmix_1(self, source, target): """special bmix() method optimized for ``r=1`` case""" B = source[16:] target[:16] = tmp = salsa20(a ^ b for a, b in izip(B, iter(source))) target[16:] = salsa20(a ^ b for a, b in izip(tmp, B)) #================================================================= # eoc #================================================================= #========================================================================== # eof #========================================================================== passlib-1.7.1/passlib/crypto/scrypt/_gen_files.py0000644000175000017500000001111313015205366023246 0ustar biscuitbiscuit00000000000000"""passlib.utils.scrypt._gen_files - meta script that generates _salsa.py""" #========================================================================== # imports #========================================================================== # core import os # pkg # local #========================================================================== # constants #========================================================================== _SALSA_OPS = [ # row = (target idx, source idx 1, source idx 2, rotate) # interpreted as salsa operation over uint32... # target = (source1+source2)<> (32 - (b)))) ##x[ 4] ^= R(x[ 0]+x[12], 7); x[ 8] ^= R(x[ 4]+x[ 0], 9); ##x[12] ^= R(x[ 8]+x[ 4],13); x[ 0] ^= R(x[12]+x[ 8],18); ( 4, 0, 12, 7), ( 8, 4, 0, 9), ( 12, 8, 4, 13), ( 0, 12, 8, 18), ##x[ 9] ^= R(x[ 5]+x[ 1], 7); x[13] ^= R(x[ 9]+x[ 5], 9); ##x[ 1] ^= R(x[13]+x[ 9],13); x[ 5] ^= R(x[ 1]+x[13],18); ( 9, 5, 1, 7), ( 13, 9, 5, 9), ( 1, 13, 9, 13), ( 5, 1, 13, 18), ##x[14] ^= R(x[10]+x[ 6], 7); x[ 2] ^= R(x[14]+x[10], 9); ##x[ 6] ^= R(x[ 2]+x[14],13); x[10] ^= R(x[ 6]+x[ 2],18); ( 14, 10, 6, 7), ( 2, 14, 10, 9), ( 6, 2, 14, 13), ( 10, 6, 2, 18), ##x[ 3] ^= R(x[15]+x[11], 7); x[ 7] ^= R(x[ 3]+x[15], 9); ##x[11] ^= R(x[ 7]+x[ 3],13); x[15] ^= R(x[11]+x[ 7],18); ( 3, 15, 11, 7), ( 7, 3, 15, 9), ( 11, 7, 3, 13), ( 15, 11, 7, 18), ##/* Operate on rows. */ ##x[ 1] ^= R(x[ 0]+x[ 3], 7); x[ 2] ^= R(x[ 1]+x[ 0], 9); ##x[ 3] ^= R(x[ 2]+x[ 1],13); x[ 0] ^= R(x[ 3]+x[ 2],18); ( 1, 0, 3, 7), ( 2, 1, 0, 9), ( 3, 2, 1, 13), ( 0, 3, 2, 18), ##x[ 6] ^= R(x[ 5]+x[ 4], 7); x[ 7] ^= R(x[ 6]+x[ 5], 9); ##x[ 4] ^= R(x[ 7]+x[ 6],13); x[ 5] ^= R(x[ 4]+x[ 7],18); ( 6, 5, 4, 7), ( 7, 6, 5, 9), ( 4, 7, 6, 13), ( 5, 4, 7, 18), ##x[11] ^= R(x[10]+x[ 9], 7); x[ 8] ^= R(x[11]+x[10], 9); ##x[ 9] ^= R(x[ 8]+x[11],13); x[10] ^= R(x[ 9]+x[ 8],18); ( 11, 10, 9, 7), ( 8, 11, 10, 9), ( 9, 8, 11, 13), ( 10, 9, 8, 18), ##x[12] ^= R(x[15]+x[14], 7); x[13] ^= R(x[12]+x[15], 9); ##x[14] ^= R(x[13]+x[12],13); x[15] ^= R(x[14]+x[13],18); ( 12, 15, 14, 7), ( 13, 12, 15, 9), ( 14, 13, 12, 13), ( 15, 14, 13, 18), ] def main(): target = os.path.join(os.path.dirname(__file__), "_salsa.py") fh = file(target, "w") write = fh.write VNAMES = ["v%d" % i for i in range(16)] PAD = " " * 4 PAD2 = " " * 8 PAD3 = " " * 12 TLIST = ", ".join("b%d" % i for i in range(16)) VLIST = ", ".join(VNAMES) kwds = dict( VLIST=VLIST, TLIST=TLIST, ) write('''\ """passlib.utils.scrypt._salsa - salsa 20/8 core, autogenerated by _gen_salsa.py""" #================================================================= # salsa function #================================================================= def salsa20(input): \"""apply the salsa20/8 core to the provided input :args input: input list containing 16 32-bit integers :returns: result list containing 16 32-bit integers \""" %(TLIST)s = input %(VLIST)s = \\ %(TLIST)s i = 0 while i < 4: ''' % kwds) for idx, (target, source1, source2, rotate) in enumerate(_SALSA_OPS): write('''\ # salsa op %(idx)d: [%(it)d] ^= ([%(is1)d]+[%(is2)d])<<<%(rot1)d t = (%(src1)s + %(src2)s) & 0xffffffff %(dst)s ^= ((t & 0x%(rmask)08x) << %(rot1)d) | (t >> %(rot2)d) ''' % dict( idx=idx, is1 = source1, is2=source2, it=target, src1=VNAMES[source1], src2=VNAMES[source2], dst=VNAMES[target], rmask=(1<<(32-rotate))-1, rot1=rotate, rot2=32-rotate, )) write('''\ i += 1 ''') for idx in range(16): write(PAD + "b%d = (b%d + v%d) & 0xffffffff\n" % (idx,idx,idx)) write('''\ return %(TLIST)s #================================================================= # eof #================================================================= ''' % kwds) if __name__ == "__main__": main() #========================================================================== # eof #========================================================================== passlib-1.7.1/passlib/crypto/_md4.py0000644000175000017500000001537113015205366020465 0ustar biscuitbiscuit00000000000000""" passlib.crypto._md4 -- fallback implementation of MD4 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 .. note:: This shouldn't be imported directly, it's merely used conditionally by ``passlib.crypto.lookup_hash()`` when a native implementation can't be found. """ #============================================================================= # imports #============================================================================= # core from binascii import hexlify import struct # site from passlib.utils.compat import 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 name = "md4" digest_size = digestsize = 16 block_size = 64 _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): if PY3: raise TypeError("expected bytes") else: # replicate behavior of hashlib under py2 content = content.encode("ascii") 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 = 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 #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/crypto/digest.py0000644000175000017500000007412213041172666021125 0ustar biscuitbiscuit00000000000000"""passlib.crypto.digest -- crytographic helpers used by the password hashes in passlib .. versionadded:: 1.7 """ #============================================================================= # imports #============================================================================= from __future__ import division # core import hashlib import logging; log = logging.getLogger(__name__) try: # new in py3.4 from hashlib import pbkdf2_hmac as _stdlib_pbkdf2_hmac if _stdlib_pbkdf2_hmac.__module__ == "hashlib": # builtin pure-python backends are slightly faster than stdlib's pure python fallback, # so only using stdlib's version if it's backed by openssl's pbkdf2_hmac() log.debug("ignoring pure-python hashlib.pbkdf2_hmac()") _stdlib_pbkdf2_hmac = None except ImportError: _stdlib_pbkdf2_hmac = None import re import os from struct import Struct from warnings import warn # site try: # https://pypi.python.org/pypi/fastpbkdf2/ from fastpbkdf2 import pbkdf2_hmac as _fast_pbkdf2_hmac except ImportError: _fast_pbkdf2_hmac = None # pkg from passlib import exc from passlib.utils import join_bytes, to_native_str, join_byte_values, to_bytes, \ SequenceMixin from passlib.utils.compat import irange, int_types, unicode_or_bytes_types, PY3 from passlib.utils.decor import memoized_property # local __all__ = [ # hash utils "lookup_hash", "HashInfo", "norm_hash_name", # hmac utils "compile_hmac", # kdfs "pbkdf1", "pbkdf2_hmac", ] #============================================================================= # generic constants #============================================================================= #: max 32-bit value MAX_UINT32 = (1 << 32) - 1 #: max 64-bit value MAX_UINT64 = (1 << 64) - 1 #============================================================================= # hash utils #============================================================================= #: list of known hash names, used by lookup_hash()'s _norm_hash_name() helper _known_hash_names = [ # format: (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"), # TODO: add sha3 to this table. # 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 of hash info instances used by lookup_hash() _hash_info_cache = {} def _get_hash_aliases(name): """ internal helper used by :func:`lookup_hash` -- normalize arbitrary hash name to hashlib format. if name not recognized, returns dummy record and issues a warning. :arg name: unnormalized name :returns: tuple with 2+ elements: ``(hashlib_name, iana_name|None, ... 0+ aliases)``. """ # normalize input orig = name if not isinstance(name, str): name = to_native_str(name, 'utf-8', 'hash name') name = re.sub("[_ /]", "-", name.strip().lower()) if name.startswith("scram-"): # helper for SCRAM protocol (see passlib.handlers.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 _known_hash_names: if name in row: return row result = check_table(name) if result: return result # try to clean name up some more m = re.match(r"(?i)^(?P[a-z]+)-?(?P\d)?-?(?P\d{3,4})?$", name) if m: # roughly follows "SHA2-256" style format, normalize representation, # and checked table. iana_name, rev, size = m.group("name", "rev", "size") if rev: iana_name += rev hashlib_name = iana_name if size: iana_name += "-" + size if rev: hashlib_name += "_" hashlib_name += size result = check_table(iana_name) if result: return result # not found in table, but roughly recognize format. use names we built up as fallback. log.info("normalizing unrecognized hash name %r => %r / %r", orig, hashlib_name, iana_name) else: # just can't make sense of it. return something iana_name = name hashlib_name = name.replace("-", "_") log.warning("normalizing unrecognized hash name and format %r => %r / %r", orig, hashlib_name, iana_name) return hashlib_name, iana_name def _get_hash_const(name): """ internal helper used by :func:`lookup_hash` -- lookup hash constructor by name :arg name: name (normalized to hashlib format, e.g. ``"sha256"``) :returns: hash constructor, e.g. ``hashlib.sha256()``; or None if hash can't be located. """ # check hashlib. for an efficient constructor if not name.startswith("_") and name not in ("new", "algorithms"): try: return getattr(hashlib, name) except AttributeError: pass # check hashlib.new() in case SSL supports the digest new_ssl_hash = hashlib.new try: # new() should throw ValueError if alg is unknown new_ssl_hash(name, b"") except ValueError: pass else: # create wrapper function # XXX: is there a faster way to wrap this? def const(msg=b""): return new_ssl_hash(name, msg) const.__name__ = name const.__module__ = "hashlib" const.__doc__ = ("wrapper for hashlib.new(%r),\n" "generated by passlib.crypto.digest.lookup_hash()") % name return const # use builtin md4 as fallback when not supported by hashlib if name == "md4": from passlib.crypto._md4 import md4 return md4 # XXX: any other modules / registries we should check? # TODO: add pysha3 support. return None def lookup_hash(digest, return_unknown=False): """ Returns a :class:`HashInfo` record containing information about a given hash function. Can be used to look up a hash constructor by name, normalize hash name representation, etc. :arg digest: This can be any of: * A string containing a :mod:`!hashlib` digest name (e.g. ``"sha256"``), * A string containing an IANA-assigned hash name, * A digest constructor function (e.g. ``hashlib.sha256``). Case is ignored, underscores are converted to hyphens, and various other cleanups are made. :param return_unknown: By default, this function will throw an :exc:`~passlib.exc.UnknownHashError` if no hash constructor can be found. However, if this flag is False, it will instead return a dummy record without a constructor function. This is mainly used by :func:`norm_hash_name`. :returns HashInfo: :class:`HashInfo` instance containing information about specified digest. Multiple calls resolving to the same hash should always return the same :class:`!HashInfo` instance. """ # check for cached entry cache = _hash_info_cache try: return cache[digest] except (KeyError, TypeError): # NOTE: TypeError is to catch 'TypeError: unhashable type' (e.g. HashInfo) pass # resolve ``digest`` to ``const`` & ``name_record`` cache_by_name = True if isinstance(digest, unicode_or_bytes_types): # normalize name name_list = _get_hash_aliases(digest) name = name_list[0] assert name # if name wasn't normalized to hashlib format, # get info for normalized name and reuse it. if name != digest: info = lookup_hash(name, return_unknown=return_unknown) if info.const is None: # pass through dummy record assert return_unknown return info cache[digest] = info return info # else look up constructor const = _get_hash_const(name) if const is None: if return_unknown: # return a dummy record (but don't cache it, so normal lookup still returns error) return HashInfo(None, name_list) else: raise exc.UnknownHashError(name) elif isinstance(digest, HashInfo): # handle border case where HashInfo is passed in. return digest elif callable(digest): # try to lookup digest based on it's self-reported name # (which we trust to be the canonical "hashlib" name) const = digest name_list = _get_hash_aliases(const().name) name = name_list[0] other_const = _get_hash_const(name) if other_const is None: # this is probably a third-party digest we don't know about, # so just pass it on through, and register reverse lookup for it's name. pass elif other_const is const: # if we got back same constructor, this is just a known stdlib constructor, # which was passed in before we had cached it by name. proceed normally. pass else: # if we got back different object, then ``const`` is something else # (such as a mock object), in which case we want to skip caching it by name, # as that would conflict with real hash. cache_by_name = False else: raise exc.ExpectedTypeError(digest, "digest name or constructor", "digest") # create new instance info = HashInfo(const, name_list) # populate cache cache[const] = info if cache_by_name: for name in name_list: if name: # (skips iana name if it's empty) assert cache.get(name) in [None, info], "%r already in cache" % name cache[name] = info return info #: UT helper for clearing internal cache lookup_hash.clear_cache = _hash_info_cache.clear def norm_hash_name(name, format="hashlib"): """Normalize hash function name (convenience wrapper for :func:`lookup_hash`). :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, this issues a warning, and then uses a heuristic to return a "best guess" name. :returns: Hash name, returned as native :class:`!str`. """ info = lookup_hash(name, return_unknown=True) if not info.const: warn("norm_hash_name(): unknown hash: %r" % (name,), exc.PasslibRuntimeWarning) if format == "hashlib": return info.name elif format == "iana": return info.iana_name else: raise ValueError("unknown format: %r" % (format,)) class HashInfo(SequenceMixin): """ Record containing information about a given hash algorithm, as returned :func:`lookup_hash`. This class exposes the following attributes: .. autoattribute:: const .. autoattribute:: digest_size .. autoattribute:: block_size .. autoattribute:: name .. autoattribute:: iana_name .. autoattribute:: aliases This object can also be treated a 3-element sequence containing ``(const, digest_size, block_size)``. """ #========================================================================= # instance attrs #========================================================================= #: Canonical / hashlib-compatible name (e.g. ``"sha256"``). name = None #: IANA assigned name (e.g. ``"sha-256"``), may be ``None`` if unknown. iana_name = None #: Tuple of other known aliases (may be empty) aliases = () #: Hash constructor function (e.g. :func:`hashlib.sha256`) const = None #: Hash's digest size digest_size = None #: Hash's block size block_size = None def __init__(self, const, names): """ initialize new instance. :arg const: hash constructor :arg names: list of 2+ names. should be list of ``(name, iana_name, ... 0+ aliases)``. names must be lower-case. only iana name may be None. """ self.name = names[0] self.iana_name = names[1] self.aliases = names[2:] self.const = const if const is None: return hash = const() self.digest_size = hash.digest_size self.block_size = hash.block_size # do sanity check on digest size if len(hash.digest()) != hash.digest_size: raise RuntimeError("%r constructor failed sanity check" % self.name) # do sanity check on name. if hash.name != self.name: warn("inconsistent digest name: %r resolved to %r, which reports name as %r" % (self.name, const, hash.name), exc.PasslibRuntimeWarning) #========================================================================= # methods #========================================================================= def __repr__(self): return " digest output``. However, if ``multipart=True``, the returned function has the signature ``hmac() -> update, finalize``, where ``update(msg)`` may be called multiple times, and ``finalize() -> digest_output`` may be repeatedly called at any point to calculate the HMAC digest so far. The returned object will also have a ``digest_info`` attribute, containing a :class:`lookup_hash` instance for the specified digest. This function exists, and has the weird signature it does, in order to squeeze as provide as much efficiency as possible, by omitting much of the setup cost and features of the stdlib :mod:`hmac` module. """ # all the following was adapted from stdlib's hmac module # resolve digest (cached) digest_info = lookup_hash(digest) const, digest_size, block_size = digest_info assert block_size >= 16, "block size too small" # prepare key if not isinstance(key, bytes): key = to_bytes(key, param="key") klen = len(key) if klen > block_size: key = const(key).digest() klen = digest_size if klen < block_size: key += b'\x00' * (block_size - klen) # create pre-initialized hash constructors _inner_copy = const(key.translate(_TRANS_36)).copy _outer_copy = const(key.translate(_TRANS_5C)).copy if multipart: # create multi-part function # NOTE: this is slightly slower than the single-shot version, # and should only be used if needed. def hmac(): """generated by compile_hmac(multipart=True)""" inner = _inner_copy() def finalize(): outer = _outer_copy() outer.update(inner.digest()) return outer.digest() return inner.update, finalize else: # single-shot function def hmac(msg): """generated by compile_hmac()""" inner = _inner_copy() inner.update(msg) outer = _outer_copy() outer.update(inner.digest()) return outer.digest() # add info attr hmac.digest_info = digest_info return hmac #============================================================================= # pbkdf1 #============================================================================= def pbkdf1(digest, secret, salt, rounds, keylen=None): """pkcs#5 password-based key derivation v1.5 :arg digest: digest name or constructor. :arg secret: secret to use when generating the key. may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). :arg salt: salt string to use when generating key. may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). :param rounds: number of rounds to use to generate key. :arg keylen: number of bytes to generate (if omitted / ``None``, uses digest's native size) :returns: raw :class:`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. """ # resolve digest const, digest_size, block_size = lookup_hash(digest) # validate secret & salt secret = to_bytes(secret, param="secret") salt = to_bytes(salt, param="salt") # validate rounds if not isinstance(rounds, int_types): raise exc.ExpectedTypeError(rounds, "int", "rounds") if rounds < 1: raise ValueError("rounds must be at least 1") # validate keylen if keylen is None: keylen = digest_size elif not isinstance(keylen, int_types): raise exc.ExpectedTypeError(keylen, "int or None", "keylen") elif keylen < 0: raise ValueError("keylen must be at least 0") elif keylen > digest_size: raise ValueError("keylength too large for digest: %r > %r" % (keylen, digest_size)) # main pbkdf1 loop block = secret + salt for _ in irange(rounds): block = const(block).digest() return block[:keylen] #============================================================================= # pbkdf2 #============================================================================= _pack_uint32 = Struct(">L").pack def pbkdf2_hmac(digest, secret, salt, rounds, keylen=None): """pkcs#5 password-based key derivation v2.0 using HMAC + arbitrary digest. :arg digest: digest name or constructor. :arg secret: passphrase to use to generate key. may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). :arg salt: salt string to use when generating key. may be :class:`!bytes` or :class:`unicode` (encoded using UTF-8). :param rounds: number of rounds to use to generate key. :arg keylen: number of bytes to generate. if omitted / ``None``, will use digest's native output size. :returns: raw bytes of generated key .. versionchanged:: 1.7 This function will use the first available of the following backends: * `fastpbk2 `_ * :func:`hashlib.pbkdf2_hmac` (only available in py2 >= 2.7.8, and py3 >= 3.4) * builtin pure-python backend See :data:`passlib.crypto.digest.PBKDF2_BACKENDS` to determine which backend(s) are in use. """ # validate secret & salt secret = to_bytes(secret, param="secret") salt = to_bytes(salt, param="salt") # resolve digest digest_info = lookup_hash(digest) digest_size = digest_info.digest_size # validate rounds if not isinstance(rounds, int_types): raise exc.ExpectedTypeError(rounds, "int", "rounds") if rounds < 1: raise ValueError("rounds must be at least 1") # validate keylen if keylen is None: keylen = digest_size elif not isinstance(keylen, int_types): raise exc.ExpectedTypeError(keylen, "int or None", "keylen") elif keylen < 1: # XXX: could allow keylen=0, but want to be compat w/ stdlib raise ValueError("keylen must be at least 1") # find smallest block count s.t. keylen <= block_count * digest_size; # make sure block count won't overflow (per pbkdf2 spec) # this corresponds to throwing error if keylen > digest_size * MAX_UINT32 # NOTE: stdlib will throw error at lower bound (keylen > MAX_SINT32) # NOTE: have do this before other backends checked, since fastpbkdf2 raises wrong error # (InvocationError, not OverflowError) block_count = (keylen + digest_size - 1) // digest_size if block_count > MAX_UINT32: raise OverflowError("keylen too long for digest") # # check for various high-speed backends # # ~3x faster than pure-python backend # NOTE: have to do this after above guards since fastpbkdf2 lacks bounds checks. if digest_info.supported_by_fastpbkdf2: return _fast_pbkdf2_hmac(digest_info.name, secret, salt, rounds, keylen) # ~1.4x faster than pure-python backend # NOTE: have to do this after fastpbkdf2 since hashlib-ssl is slower, # will support larger number of hashes. if digest_info.supported_by_hashlib_pbkdf2: return _stdlib_pbkdf2_hmac(digest_info.name, secret, salt, rounds, keylen) # # otherwise use our own implementation # # generated keyed hmac keyed_hmac = compile_hmac(digest, secret) # get helper to calculate pbkdf2 inner loop efficiently calc_block = _get_pbkdf2_looper(digest_size) # assemble & return result return join_bytes( calc_block(keyed_hmac, keyed_hmac(salt + _pack_uint32(i)), rounds) for i in irange(1, block_count + 1) )[:keylen] #------------------------------------------------------------------------------------- # pick best choice for pure-python helper # TODO: consider some alternatives, such as C-accelerated xor_bytes helper if available #------------------------------------------------------------------------------------- # NOTE: this env var is only present to support the admin/benchmark_pbkdf2 script _force_backend = os.environ.get("PASSLIB_PBKDF2_BACKEND") or "any" if PY3 and _force_backend in ["any", "from-bytes"]: from functools import partial def _get_pbkdf2_looper(digest_size): return partial(_pbkdf2_looper, digest_size) def _pbkdf2_looper(digest_size, keyed_hmac, digest, rounds): """ py3-only implementation of pbkdf2 inner loop; uses 'int.from_bytes' + integer XOR """ from_bytes = int.from_bytes BIG = "big" # endianess doesn't matter, just has to be consistent accum = from_bytes(digest, BIG) for _ in irange(rounds - 1): digest = keyed_hmac(digest) accum ^= from_bytes(digest, BIG) return accum.to_bytes(digest_size, BIG) _builtin_backend = "from-bytes" elif _force_backend in ["any", "unpack", "from-bytes"]: from struct import Struct from passlib.utils import sys_bits _have_64_bit = (sys_bits >= 64) #: cache used by _get_pbkdf2_looper _looper_cache = {} def _get_pbkdf2_looper(digest_size): """ We want a helper function which performs equivalent of the following:: def helper(keyed_hmac, digest, rounds): accum = digest for _ in irange(rounds - 1): digest = keyed_hmac(digest) accum ^= digest return accum However, no efficient way to implement "bytes ^ bytes" in python. Instead, using approach where we dynamically compile a helper function based on digest size. Instead of a single `accum` var, this helper breaks the digest into a series of integers. It stores these in a series of`accum_` vars, and performs `accum ^= digest` by unpacking digest and perform xor for each "accum_ ^= digest_". this keeps everything in locals, avoiding excessive list creation, encoding or decoding, etc. :param digest_size: digest size to compile for, in bytes. (must be multiple of 4). :return: helper function with call signature outlined above. """ # # cache helpers # try: return _looper_cache[digest_size] except KeyError: pass # # figure out most efficient struct format to unpack digest into list of native ints # if _have_64_bit and not digest_size & 0x7: # digest size multiple of 8, on a 64 bit system -- use array of UINT64 count = (digest_size >> 3) fmt = "=%dQ" % count elif not digest_size & 0x3: if _have_64_bit: # digest size multiple of 4, on a 64 bit system -- use array of UINT64 + 1 UINT32 count = (digest_size >> 3) fmt = "=%dQI" % count count += 1 else: # digest size multiple of 4, on a 32 bit system -- use array of UINT32 count = (digest_size >> 2) fmt = "=%dI" % count else: # stopping here, cause no known hashes have digest size that isn't multiple of 4 bytes. # if needed, could go crazy w/ "H" & "B" raise NotImplementedError("unsupported digest size: %d" % digest_size) struct = Struct(fmt) # # build helper source # tdict = dict( digest_size=digest_size, accum_vars=", ".join("acc_%d" % i for i in irange(count)), digest_vars=", ".join("dig_%d" % i for i in irange(count)), ) # head of function source = ( "def helper(keyed_hmac, digest, rounds):\n" " '''pbkdf2 loop helper for digest_size={digest_size}'''\n" " unpack_digest = struct.unpack\n" " {accum_vars} = unpack_digest(digest)\n" " for _ in irange(1, rounds):\n" " digest = keyed_hmac(digest)\n" " {digest_vars} = unpack_digest(digest)\n" ).format(**tdict) # xor digest for i in irange(count): source += " acc_%d ^= dig_%d\n" % (i, i) # return result source += " return struct.pack({accum_vars})\n".format(**tdict) # # compile helper # code = compile(source, "", "exec") gdict = dict(irange=irange, struct=struct) ldict = dict() eval(code, gdict, ldict) helper = ldict['helper'] if __debug__: helper.__source__ = source # # store in cache # _looper_cache[digest_size] = helper return helper _builtin_backend = "unpack" else: assert _force_backend in ["any", "hexlify"] # XXX: older & slower approach that used int(hexlify()), # keeping it around for a little while just for benchmarking. from binascii import hexlify as _hexlify from passlib.utils import int_to_bytes def _get_pbkdf2_looper(digest_size): return _pbkdf2_looper def _pbkdf2_looper(keyed_hmac, digest, rounds): hexlify = _hexlify accum = int(hexlify(digest), 16) for _ in irange(rounds - 1): digest = keyed_hmac(digest) accum ^= int(hexlify(digest), 16) return int_to_bytes(accum, len(digest)) _builtin_backend = "hexlify" # helper for benchmark script -- disable hashlib, fastpbkdf2 support if builtin requested if _force_backend == _builtin_backend: _fast_pbkdf2_hmac = _stdlib_pbkdf2_hmac = None # expose info about what backends are active PBKDF2_BACKENDS = [b for b in [ "fastpbkdf2" if _fast_pbkdf2_hmac else None, "hashlib-ssl" if _stdlib_pbkdf2_hmac else None, "builtin-" + _builtin_backend ] if b] # *very* rough estimate of relative speed (compared to sha256 using 'unpack' backend on 64bit arch) if "fastpbkdf2" in PBKDF2_BACKENDS: PBKDF2_SPEED_FACTOR = 3 elif "hashlib-ssl" in PBKDF2_BACKENDS: PBKDF2_SPEED_FACTOR = 1.4 else: # remaining backends have *some* difference in performance, but not enough to matter PBKDF2_SPEED_FACTOR = 1 #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/crypto/des.py0000644000175000017500000014524613015205366020422 0ustar biscuitbiscuit00000000000000"""passlib.crypto.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 join_byte_values, byte_elem_value, \ irange, irange, int_types # local __all__ = [ "expand_des_key", "des_encrypt_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 #============================================================================= # FIXME: more properly named _uint8_struct... _uint64_struct = struct.Struct(">Q") 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(b'\x00' + 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. # XXX: could make parity-restoring optionally available via flag ##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) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/pwd.py0000644000175000017500000006763213020045766017125 0ustar biscuitbiscuit00000000000000"""passlib.pwd -- password generation helpers""" #============================================================================= # imports #============================================================================= from __future__ import absolute_import, division, print_function, unicode_literals # core import codecs from collections import defaultdict, MutableMapping from math import ceil, log as logf import logging; log = logging.getLogger(__name__) import pkg_resources import os # site # pkg from passlib import exc from passlib.utils.compat import PY2, irange, itervalues, int_types from passlib.utils import rng, getrandstr, to_unicode from passlib.utils.decor import memoized_property # local __all__ = [ "genword", "default_charsets", "genphrase", "default_wordsets", ] #============================================================================= # constants #============================================================================= # XXX: rename / publically document this map? entropy_aliases = dict( # barest protection from throttled online attack unsafe=12, # some protection from unthrottled online attack weak=24, # some protection from offline attacks fair=36, # reasonable protection from offline attacks strong=48, # very good protection from offline attacks secure=60, ) #============================================================================= # internal helpers #============================================================================= def _superclasses(obj, cls): """return remaining classes in object's MRO after cls""" mro = type(obj).__mro__ return mro[mro.index(cls)+1:] def _self_info_rate(source): """ returns 'rate of self-information' -- i.e. average (per-symbol) entropy of the sequence **source**, where probability of a given symbol occurring is calculated based on the number of occurrences within the sequence itself. if all elements of the source are unique, this should equal ``log(len(source), 2)``. :arg source: iterable containing 0+ symbols (e.g. list of strings or ints, string of characters, etc). :returns: float bits of entropy """ try: size = len(source) except TypeError: # if len() doesn't work, calculate size by summing counts later size = None counts = defaultdict(int) for char in source: counts[char] += 1 if size is None: values = counts.values() size = sum(values) else: values = itervalues(counts) if not size: return 0 # NOTE: the following performs ``- sum(value / size * logf(value / size, 2) for value in values)``, # it just does so with as much pulled out of the sum() loop as possible... return logf(size, 2) - sum(value * logf(value, 2) for value in values) / size # def _total_self_info(source): # """ # return total self-entropy of a sequence # (the average entropy per symbol * size of sequence) # """ # return _self_info_rate(source) * len(source) def _open_asset_path(path, encoding=None): """ :param asset_path: string containing absolute path to file, or package-relative path using format ``"python.module:relative/file/path"``. :returns: filehandle opened in 'rb' mode (unless encoding explicitly specified) """ if encoding: return codecs.getreader(encoding)(_open_asset_path(path)) if os.path.isabs(path): return open(path, "rb") package, sep, subpath = path.partition(":") if not sep: raise ValueError("asset path must be absolute file path " "or use 'pkg.name:sub/path' format: %r" % (path,)) return pkg_resources.resource_stream(package, subpath) #: type aliases _sequence_types = (list, tuple) _set_types = (set, frozenset) #: set of elements that ensure_unique() has validated already. _ensure_unique_cache = set() def _ensure_unique(source, param="source"): """ helper for generators -- Throws ValueError if source elements aren't unique. Error message will display (abbreviated) repr of the duplicates in a string/list """ # check cache to speed things up for frozensets / tuples / strings cache = _ensure_unique_cache hashable = True try: if source in cache: return True except TypeError: hashable = False # check if it has dup elements if isinstance(source, _set_types) or len(set(source)) == len(source): if hashable: try: cache.add(source) except TypeError: # XXX: under pypy, "list() in set()" above doesn't throw TypeError, # but trying to add unhashable it to a set *does*. pass return True # build list of duplicate values seen = set() dups = set() for elem in source: (dups if elem in seen else seen).add(elem) dups = sorted(dups) trunc = 8 if len(dups) > trunc: trunc = 5 dup_repr = ", ".join(repr(str(word)) for word in dups[:trunc]) if len(dups) > trunc: dup_repr += ", ... plus %d others" % (len(dups) - trunc) # throw error raise ValueError("`%s` cannot contain duplicate elements: %s" % (param, dup_repr)) #============================================================================= # base generator class #============================================================================= class SequenceGenerator(object): """ Base class used by word & phrase generators. These objects take a series of options, corresponding to those of the :func:`generate` function. They act as callables which can be used to generate a password or a list of 1+ passwords. They also expose some read-only informational attributes. Parameters ---------- :param entropy: Optionally specify the amount of entropy the resulting passwords should contain (as measured with respect to the generator itself). This will be used to auto-calculate the required password size. :param length: Optionally specify the length of password to generate, measured as count of whatever symbols the subclass uses (characters or words). Note if ``entropy`` requires a larger minimum length, that will be used instead. :param rng: Optionally provide a custom RNG source to use. Should be an instance of :class:`random.Random`, defaults to :class:`random.SystemRandom`. Attributes ---------- .. autoattribute:: length .. autoattribute:: symbol_count .. autoattribute:: entropy_per_symbol .. autoattribute:: entropy Subclassing ----------- Subclasses must implement the ``.__next__()`` method, and set ``.symbol_count`` before calling base ``__init__`` method. """ #============================================================================= # instance attrs #============================================================================= #: requested size of final password length = None #: requested entropy of final password requested_entropy = "strong" #: random number source to use rng = rng #: number of potential symbols (must be filled in by subclass) symbol_count = None #============================================================================= # init #============================================================================= def __init__(self, entropy=None, length=None, rng=None, **kwds): # make sure subclass set things up correctly assert self.symbol_count is not None, "subclass must set .symbol_count" # init length & requested entropy if entropy is not None or length is None: if entropy is None: entropy = self.requested_entropy entropy = entropy_aliases.get(entropy, entropy) if entropy <= 0: raise ValueError("`entropy` must be positive number") min_length = int(ceil(entropy / self.entropy_per_symbol)) if length is None or length < min_length: length = min_length self.requested_entropy = entropy if length < 1: raise ValueError("`length` must be positive integer") self.length = length # init other common options if rng is not None: self.rng = rng # hand off to parent if kwds and _superclasses(self, SequenceGenerator) == (object,): raise TypeError("Unexpected keyword(s): %s" % ", ".join(kwds.keys())) super(SequenceGenerator, self).__init__(**kwds) #============================================================================= # informational helpers #============================================================================= @memoized_property def entropy_per_symbol(self): """ Average entropy per symbol (assuming all symbols have equal probability) """ return logf(self.symbol_count, 2) @memoized_property def entropy(self): """ Effective entropy of generated passwords. This value will always be a multiple of :attr:`entropy_per_symbol`. If entropy is specified in constructor, :attr:`length` will be chosen so so that this value is the smallest multiple >= :attr:`requested_entropy`. """ return self.length * self.entropy_per_symbol #============================================================================= # generation #============================================================================= def __next__(self): """main generation function, should create one password/phrase""" raise NotImplementedError("implement in subclass") def __call__(self, returns=None): """ frontend used by genword() / genphrase() to create passwords """ if returns is None: return next(self) elif isinstance(returns, int_types): return [next(self) for _ in irange(returns)] elif returns is iter: return self else: raise exc.ExpectedTypeError(returns, ", int, or ", "returns") def __iter__(self): return self if PY2: def next(self): return self.__next__() #============================================================================= # eoc #============================================================================= #============================================================================= # default charsets #============================================================================= #: global dict of predefined characters sets default_charsets = dict( # ascii letters, digits, and some punctuation ascii_72='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*?/', # ascii letters and digits ascii_62='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', # ascii_50, without visually similar '1IiLl', '0Oo', '5S', '8B' ascii_50='234679abcdefghjkmnpqrstuvwxyzACDEFGHJKMNPQRTUVWXYZ', # lower case hexadecimal hex='0123456789abcdef', ) #============================================================================= # password generator #============================================================================= class WordGenerator(SequenceGenerator): """ Class which generates passwords by randomly choosing from a string of unique characters. Parameters ---------- :param chars: custom character string to draw from. :param charset: predefined charset to draw from. :param \*\*kwds: all other keywords passed to the :class:`SequenceGenerator` parent class. Attributes ---------- .. autoattribute:: chars .. autoattribute:: charset .. autoattribute:: default_charsets """ #============================================================================= # instance attrs #============================================================================= #: Predefined character set in use (set to None for instances using custom 'chars') charset = "ascii_62" #: string of chars to draw from -- usually filled in from charset chars = None #============================================================================= # init #============================================================================= def __init__(self, chars=None, charset=None, **kwds): # init chars and charset if chars: if charset: raise TypeError("`chars` and `charset` are mutually exclusive") else: if not charset: charset = self.charset assert charset chars = default_charsets[charset] self.charset = charset chars = to_unicode(chars, param="chars") _ensure_unique(chars, param="chars") self.chars = chars # hand off to parent super(WordGenerator, self).__init__(**kwds) # log.debug("WordGenerator(): entropy/char=%r", self.entropy_per_symbol) #============================================================================= # informational helpers #============================================================================= @memoized_property def symbol_count(self): return len(self.chars) #============================================================================= # generation #============================================================================= def __next__(self): # XXX: could do things like optionally ensure certain character groups # (e.g. letters & punctuation) are included return getrandstr(self.rng, self.chars, self.length) #============================================================================= # eoc #============================================================================= def genword(entropy=None, length=None, returns=None, **kwds): """Generate one or more random passwords. This function uses :mod:`random.SystemRandom` to generate one or more passwords using various character sets. The complexity of the password can be specified by size, or by the desired amount of entropy. Usage Example:: >>> # generate a random alphanumeric string with 48 bits of entropy (the default) >>> from passlib import pwd >>> pwd.genword() 'DnBHvDjMK6' >>> # generate a random hexadecimal string with 52 bits of entropy >>> pwd.genword(entropy=52, charset="hex") '310f1a7ac793f' :param entropy: Strength of resulting password, measured in 'guessing entropy' bits. An appropriate **length** value will be calculated based on the requested entropy amount, and the size of the character set. This can be a positive integer, or one of the following preset strings: ``"weak"`` (24), ``"fair"`` (36), ``"strong"`` (48), and ``"secure"`` (56). If neither this or **length** is specified, **entropy** will default to ``"strong"`` (48). :param length: Size of resulting password, measured in characters. If omitted, the size is auto-calculated based on the **entropy** parameter. If both **entropy** and **length** are specified, the stronger value will be used. :param returns: Controls what this function returns: * If ``None`` (the default), this function will generate a single password. * If an integer, this function will return a list containing that many passwords. * If the ``iter`` constant, will return an iterator that yields passwords. :param chars: Optionally specify custom string of characters to use when randomly generating a password. This option cannot be combined with **charset**. :param charset: The predefined character set to draw from (if not specified by **chars**). There are currently four presets available: * ``"ascii_62"`` (the default) -- all digits and ascii upper & lowercase letters. Provides ~5.95 entropy per character. * ``"ascii_50"`` -- subset which excludes visually similar characters (``1IiLl0Oo5S8B``). Provides ~5.64 entropy per character. * ``"ascii_72"`` -- all digits and ascii upper & lowercase letters, as well as some punctuation. Provides ~6.17 entropy per character. * ``"hex"`` -- Lower case hexadecimal. Providers 4 bits of entropy per character. :returns: :class:`!unicode` string containing randomly generated password; or list of 1+ passwords if :samp:`returns={int}` is specified. """ gen = WordGenerator(length=length, entropy=entropy, **kwds) return gen(returns) #============================================================================= # default wordsets #============================================================================= def _load_wordset(asset_path): """ load wordset from compressed datafile within package data. file should be utf-8 encoded :param asset_path: string containing absolute path to wordset file, or "python.module:relative/file/path". :returns: tuple of words, as loaded from specified words file. """ # open resource file, convert to tuple of words (strip blank lines & ws) with _open_asset_path(asset_path, "utf-8") as fh: gen = (word.strip() for word in fh) words = tuple(word for word in gen if word) # NOTE: works but not used # # detect if file uses " " format, and strip numeric prefix # def extract(row): # idx, word = row.replace("\t", " ").split(" ", 1) # if not idx.isdigit(): # raise ValueError("row is not dice index + word") # return word # try: # extract(words[-1]) # except ValueError: # pass # else: # words = tuple(extract(word) for word in words) log.debug("loaded %d-element wordset from %r", len(words), asset_path) return words class WordsetDict(MutableMapping): """ Special mapping used to store dictionary of wordsets. Different from a regular dict in that some wordsets may be lazy-loaded from an asset path. """ #: dict of key -> asset path paths = None #: dict of key -> value _loaded = None def __init__(self, *args, **kwds): self.paths = {} self._loaded = {} super(WordsetDict, self).__init__(*args, **kwds) def __getitem__(self, key): try: return self._loaded[key] except KeyError: pass path = self.paths[key] value = self._loaded[key] = _load_wordset(path) return value def set_path(self, key, path): """ set asset path to lazy-load wordset from. """ self.paths[key] = path def __setitem__(self, key, value): self._loaded[key] = value def __delitem__(self, key): if key in self: del self._loaded[key] self.paths.pop(key, None) else: del self.paths[key] @property def _keyset(self): keys = set(self._loaded) keys.update(self.paths) return keys def __iter__(self): return iter(self._keyset) def __len__(self): return len(self._keyset) # NOTE: speeds things up, and prevents contains from lazy-loading def __contains__(self, key): return key in self._loaded or key in self.paths #: dict of predefined word sets. #: key is name of wordset, value should be sequence of words. default_wordsets = WordsetDict() # register the wordsets built into passlib for name in "eff_long eff_short eff_prefixed bip39".split(): default_wordsets.set_path(name, "passlib:_data/wordsets/%s.txt" % name) #============================================================================= # passphrase generator #============================================================================= class PhraseGenerator(SequenceGenerator): """class which generates passphrases by randomly choosing from a list of unique words. :param wordset: wordset to draw from. :param preset: name of preset wordlist to use instead of ``wordset``. :param spaces: whether to insert spaces between words in output (defaults to ``True``). :param \*\*kwds: all other keywords passed to the :class:`SequenceGenerator` parent class. .. autoattribute:: wordset """ #============================================================================= # instance attrs #============================================================================= #: predefined wordset to use wordset = "eff_long" #: list of words to draw from words = None #: separator to use when joining words sep = " " #============================================================================= # init #============================================================================= def __init__(self, wordset=None, words=None, sep=None, **kwds): # load wordset if words is not None: if wordset is not None: raise TypeError("`words` and `wordset` are mutually exclusive") else: if wordset is None: wordset = self.wordset assert wordset words = default_wordsets[wordset] self.wordset = wordset # init words if not isinstance(words, _sequence_types): words = tuple(words) _ensure_unique(words, param="words") self.words = words # init separator if sep is None: sep = self.sep sep = to_unicode(sep, param="sep") self.sep = sep # hand off to parent super(PhraseGenerator, self).__init__(**kwds) ##log.debug("PhraseGenerator(): entropy/word=%r entropy/char=%r min_chars=%r", ## self.entropy_per_symbol, self.entropy_per_char, self.min_chars) #============================================================================= # informational helpers #============================================================================= @memoized_property def symbol_count(self): return len(self.words) #============================================================================= # generation #============================================================================= def __next__(self): words = (self.rng.choice(self.words) for _ in irange(self.length)) return self.sep.join(words) #============================================================================= # eoc #============================================================================= def genphrase(entropy=None, length=None, returns=None, **kwds): """Generate one or more random password / passphrases. This function uses :mod:`random.SystemRandom` to generate one or more passwords; it can be configured to generate alphanumeric passwords, or full english phrases. The complexity of the password can be specified by size, or by the desired amount of entropy. Usage Example:: >>> # generate random phrase with 48 bits of entropy >>> from passlib import pwd >>> pwd.genphrase() 'gangly robbing salt shove' >>> # generate a random phrase with 52 bits of entropy >>> # using a particular wordset >>> pwd.genword(entropy=52, wordset="bip39") 'wheat dilemma reward rescue diary' :param entropy: Strength of resulting password, measured in 'guessing entropy' bits. An appropriate **length** value will be calculated based on the requested entropy amount, and the size of the word set. This can be a positive integer, or one of the following preset strings: ``"weak"`` (24), ``"fair"`` (36), ``"strong"`` (48), and ``"secure"`` (56). If neither this or **length** is specified, **entropy** will default to ``"strong"`` (48). :param length: Length of resulting password, measured in words. If omitted, the size is auto-calculated based on the **entropy** parameter. If both **entropy** and **length** are specified, the stronger value will be used. :param returns: Controls what this function returns: * If ``None`` (the default), this function will generate a single password. * If an integer, this function will return a list containing that many passwords. * If the ``iter`` builtin, will return an iterator that yields passwords. :param words: Optionally specifies a list/set of words to use when randomly generating a passphrase. This option cannot be combined with **wordset**. :param wordset: The predefined word set to draw from (if not specified by **words**). There are currently four presets available: ``"eff_long"`` (the default) Wordset containing 7776 english words of ~7 letters. Constructed by the EFF, it offers ~12.9 bits of entropy per word. This wordset (and the other ``"eff_"`` wordsets) were `created by the EFF `_ to aid in generating passwords. See their announcement page for more details about the design & properties of these wordsets. ``"eff_short"`` Wordset containing 1296 english words of ~4.5 letters. Constructed by the EFF, it offers ~10.3 bits of entropy per word. ``"eff_prefixed"`` Wordset containing 1296 english words of ~8 letters, selected so that they each have a unique 3-character prefix. Constructed by the EFF, it offers ~10.3 bits of entropy per word. ``"bip39"`` Wordset of 2048 english words of ~5 letters, selected so that they each have a unique 4-character prefix. Published as part of Bitcoin's `BIP 39 `_, this wordset has exactly 11 bits of entropy per word. This list offers words that are typically shorter than ``"eff_long"`` (at the cost of slightly less entropy); and much shorter than ``"eff_prefixed"`` (at the cost of a longer unique prefix). :param sep: Optional separator to use when joining words. Defaults to ``" "`` (a space), but can be an empty string, a hyphen, etc. :returns: :class:`!unicode` string containing randomly generated passphrase; or list of 1+ passphrases if :samp:`returns={int}` is specified. """ gen = PhraseGenerator(entropy=entropy, length=length, **kwds) return gen(returns) #============================================================================= # strength measurement # # NOTE: # for a little while, had rough draft of password strength measurement alg here. # but not sure if there's value in yet another measurement algorithm, # that's not just duplicating the effort of libraries like zxcbn. # may revive it later, but for now, leaving some refs to others out there: # * NIST 800-63 has simple alg # * zxcvbn (https://tech.dropbox.com/2012/04/zxcvbn-realistic-password-strength-estimation/) # might also be good, and has approach similar to composite approach i was already thinking about, # but much more well thought out. # * passfault (https://github.com/c-a-m/passfault) looks thorough, # but may have licensing issues, plus porting to python looks like very big job :( # * give a look at running things through zlib - might be able to cheaply # catch extra redundancies. #============================================================================= #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/_setup/0000755000175000017500000000000013043774617017253 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/_setup/__init__.py0000644000175000017500000000010012214647077021351 0ustar biscuitbiscuit00000000000000"""passlib.setup - helpers used by passlib's setup.py script""" passlib-1.7.1/passlib/_setup/stamp.py0000644000175000017500000001103513021602603020726 0ustar biscuitbiscuit00000000000000"""update version string during build""" #============================================================================= # imports #============================================================================= from __future__ import absolute_import, division, print_function # core from distutils.dist import Distribution import os import re import subprocess import time # pkg # local __all__ = [ "stamp_source", "stamp_distutils_output", "append_hg_revision", "as_bool", ] #============================================================================= # helpers #============================================================================= def get_command_class(opts, name): return opts['cmdclass'].get(name) or Distribution().get_command_class(name) def get_command_options(opts, command): return opts.setdefault("options", {}).setdefault(command, {}) def set_command_options(opts, command, **kwds): get_command_options(opts, command).update(kwds) def _get_file(path): with open(path, "r") as fh: return fh.read() def _replace_file(path, content, dry_run=False): if dry_run: return if os.path.exists(path): # sdist likes to use hardlinks, have to remove them first, # or we modify *source* file os.unlink(path) with open(path, "w") as fh: fh.write(content) def stamp_source(base_dir, version, dry_run=False): """ update version info in passlib source """ # # update version string in toplevel package source # path = os.path.join(base_dir, "passlib", "__init__.py") content = _get_file(path) content, count = re.subn('(?m)^__version__\s*=.*$', '__version__ = ' + repr(version), content) assert count == 1, "failed to replace version string" _replace_file(path, content, dry_run=dry_run) # # update flag in setup.py # (not present when called from bdist_wheel, etc) # path = os.path.join(base_dir, "setup.py") if os.path.exists(path): content = _get_file(path) content, count = re.subn('(?m)^stamp_build\s*=.*$', 'stamp_build = False', content) assert count == 1, "failed to update 'stamp_build' flag" _replace_file(path, content, dry_run=dry_run) 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 def as_bool(value): return (value or "").lower() in "yes y true t 1".split() def append_hg_revision(version): # call HG via subprocess # NOTE: for py26 compat, using Popen() instead of check_output() try: proc = subprocess.Popen(["hg", "tip", "--template", "{date(date, '%Y%m%d%H%M%S')}+hg.{node|short}"], stdout=subprocess.PIPE) stamp, _ = proc.communicate() if proc.returncode: raise subprocess.CalledProcessError(1, []) stamp = stamp.decode("ascii") except (OSError, subprocess.CalledProcessError): # fallback - just use build date stamp = time.strftime("%Y%m%d%H%M%S") # modify version if version.endswith((".dev0", ".post0")): version = version[:-1] + stamp else: version += ".post" + stamp return version def install_build_py_exclude(opts): _build_py = get_command_class(opts, "build_py") class build_py(_build_py): user_options = _build_py.user_options + [ ("exclude-packages=", None, "exclude packages from builds"), ] exclude_packages = None def finalize_options(self): _build_py.finalize_options(self) target = self.packages for package in self.exclude_packages or []: if package in target: target.remove(package) opts['cmdclass']['build_py'] = build_py #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/0000755000175000017500000000000013043774617017554 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/handlers/fshp.py0000644000175000017500000001716713015205366021067 0ustar biscuitbiscuit00000000000000"""passlib.handlers.fshp """ #============================================================================= # imports #============================================================================= # core from base64 import b64encode, b64decode import re import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import to_unicode import passlib.utils.handlers as uh from passlib.utils.compat import bascii_to_str, iteritems, u,\ unicode from passlib.crypto.digest 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.using` method accepts 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 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)] ) #=================================================================== # configuration #=================================================================== @classmethod def using(cls, variant=None, **kwds): subcls = super(fshp, cls).using(**kwds) if variant is not None: subcls.default_variant = cls._norm_variant(variant) return subcls #=================================================================== # 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 if variant is not None: variant = self._norm_variant(variant) elif self.use_defaults: variant = self.default_variant assert self._norm_variant(variant) == variant, "invalid default variant: %r" % (variant,) else: raise TypeError("no variant specified") self.variant = variant super(fshp, self).__init__(**kwds) @classmethod def _norm_variant(cls, variant): if isinstance(variant, bytes): variant = variant.decode("ascii") if isinstance(variant, unicode): try: variant = cls._variant_aliases[variant] except KeyError: raise ValueError("invalid fshp variant") if not isinstance(variant, int): raise TypeError("fshp variant must be int or known alias") if variant not in cls._variant_info: raise 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) def to_string(self): chk = self.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( digest=self.checksum_alg, secret=self.salt, salt=secret, rounds=self.rounds, keylen=self.checksum_size, ) #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/__init__.py0000644000175000017500000000012612214647077021662 0ustar biscuitbiscuit00000000000000"""passlib.handlers -- holds implementations of all passlib's builtin hash formats""" passlib-1.7.1/passlib/handlers/ldap_digests.py0000644000175000017500000002357513015205366022571 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 # site # pkg from passlib.handlers.misc import plaintext from passlib.utils import unix_crypt_schemes, to_unicode from passlib.utils.compat import uascii_to_str, unicode, u from passlib.utils.decor import classproperty 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 _hash_func = None # required - hash function _hash_regex = None # required - regexp to recognize hash 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 + 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.hash` 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.hash` 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.using` method accepts 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 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})$")) 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.using` method accepts 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 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})$")) 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.hash`, :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+\}.*$")) @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls): # Overridding plaintext.genconfig() since it returns "", # but have to return non-empty value due to identify() below return "!" @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.7.1/passlib/handlers/scrypt.py0000644000175000017500000003350213015205366021442 0ustar biscuitbiscuit00000000000000"""passlib.handlers.scrypt -- scrypt password hash""" #============================================================================= # imports #============================================================================= from __future__ import with_statement, absolute_import # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib.crypto import scrypt as _scrypt from passlib.utils import h64, to_bytes from passlib.utils.binary import h64, b64s_decode, b64s_encode from passlib.utils.compat import u, bascii_to_str, suppress_cause from passlib.utils.decor import classproperty import passlib.utils.handlers as uh # local __all__ = [ "scrypt", ] #============================================================================= # scrypt format identifiers #============================================================================= IDENT_SCRYPT = u("$scrypt$") # identifier used by passlib IDENT_7 = u("$7$") # used by official scrypt spec _UDOLLAR = u("$") #============================================================================= # handler #============================================================================= class scrypt(uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.HasManyIdents, uh.GenericHandler): """This class implements an SCrypt-based password [#scrypt-home]_ hash, and follows the :ref:`password-hash-api`. It supports a variable-length salt, a variable number of rounds, as well as some custom tuning parameters unique to scrypt (see below). The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If specified, the length must be between 0-1024 bytes. If not specified, one will be auto-generated (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 16, but must be within ``range(1,32)``. .. warning:: Unlike many hash algorithms, increasing the rounds value will increase both the time *and memory* required to hash a password. :type block_size: int :param block_size: Optional block size to pass to scrypt hash function (the ``r`` parameter). Useful for tuning scrypt to optimal performance for your CPU architecture. Defaults to 8. :type parallelism: int :param parallelism: Optional parallelism to pass to scrypt hash function (the ``p`` parameter). Defaults to 1. :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. .. note:: The underlying scrypt hash function has a number of limitations on it's parameter values, which forbids certain combinations of settings. The requirements are: * ``linear_rounds = 2**`` * ``linear_rounds < 2**(16 * block_size)`` * ``block_size * parallelism <= 2**30-1`` .. todo:: This class currently does not support configuring default values for ``block_size`` or ``parallelism`` via a :class:`~passlib.context.CryptContext` configuration. """ #=================================================================== # class attrs #=================================================================== #------------------------ # PasswordHash #------------------------ name = "scrypt" setting_kwds = ("ident", "salt", "salt_size", "rounds", "block_size", "parallelism") #------------------------ # GenericHandler #------------------------ # NOTE: scrypt supports arbitrary output sizes. since it's output runs through # pbkdf2-hmac-sha256 before returning, and this could be raised eventually... # but a 256-bit digest is more than sufficient for password hashing. # XXX: make checksum size configurable? could merge w/ argon2 code that does this. checksum_size = 32 #------------------------ # HasManyIdents #------------------------ default_ident = IDENT_SCRYPT ident_values = (IDENT_SCRYPT, IDENT_7) #------------------------ # HasRawSalt #------------------------ default_salt_size = 16 max_salt_size = 1024 #------------------------ # HasRounds #------------------------ # TODO: would like to dynamically pick this based on system default_rounds = 16 min_rounds = 1 max_rounds = 31 # limited by scrypt alg rounds_cost = "log2" # TODO: make default block size configurable via using(), and deprecatable via .needs_update() #=================================================================== # instance attrs #=================================================================== #: default parallelism setting (min=1 currently hardcoded in mixin) parallelism = 1 #: default block size setting block_size = 8 #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, block_size=None, **kwds): subcls = super(scrypt, cls).using(**kwds) if block_size is not None: if isinstance(block_size, uh.native_string_types): block_size = int(block_size) subcls.block_size = subcls._norm_block_size(block_size, relaxed=kwds.get("relaxed")) # make sure param combination is valid for scrypt() try: _scrypt.validate(1 << cls.default_rounds, cls.block_size, cls.parallelism) except ValueError as err: raise suppress_cause(ValueError("scrypt: invalid settings combination: " + str(err))) return subcls #=================================================================== # parsing #=================================================================== @classmethod def from_string(cls, hash): return cls(**cls.parse(hash)) @classmethod def parse(cls, hash): ident, suffix = cls._parse_ident(hash) func = getattr(cls, "_parse_%s_string" % ident.strip(_UDOLLAR), None) if func: return func(suffix) else: raise uh.exc.InvalidHashError(cls) # # passlib's format: # $scrypt$ln=,r=,p=

$[$] # where: # logN, r, p -- decimal-encoded positive integer, no zero-padding # logN -- log cost setting # r -- block size setting (usually 8) # p -- parallelism setting (usually 1) # salt, digest -- b64-nopad encoded bytes # @classmethod def _parse_scrypt_string(cls, suffix): # break params, salt, and digest sections parts = suffix.split("$") if len(parts) == 3: params, salt, digest = parts elif len(parts) == 2: params, salt = parts digest = None else: raise uh.exc.MalformedHashError(cls, "malformed hash") # break params apart parts = params.split(",") if len(parts) == 3: nstr, bstr, pstr = parts assert nstr.startswith("ln=") assert bstr.startswith("r=") assert pstr.startswith("p=") else: raise uh.exc.MalformedHashError(cls, "malformed settings field") return dict( ident=IDENT_SCRYPT, rounds=int(nstr[3:]), block_size=int(bstr[2:]), parallelism=int(pstr[2:]), salt=b64s_decode(salt.encode("ascii")), checksum=b64s_decode(digest.encode("ascii")) if digest else None, ) # # official format specification defined at # https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt # format: # $7$[$] # 0 12345 67890 1 # where: # All bytes use h64-little-endian encoding # N: 6-bit log cost setting # r: 30-bit block size setting # p: 30-bit parallelism setting # salt: variable length salt bytes # digest: fixed 32-byte digest # @classmethod def _parse_7_string(cls, suffix): # XXX: annoyingly, official spec embeds salt *raw*, yet doesn't specify a hash encoding. # so assuming only h64 chars are valid for salt, and are ASCII encoded. # split into params & digest parts = suffix.encode("ascii").split(b"$") if len(parts) == 2: params, digest = parts elif len(parts) == 1: params, = parts digest = None else: raise uh.exc.MalformedHashError() # parse params & return if len(params) < 11: raise uh.exc.MalformedHashError(cls, "params field too short") return dict( ident=IDENT_7, rounds=h64.decode_int6(params[:1]), block_size=h64.decode_int30(params[1:6]), parallelism=h64.decode_int30(params[6:11]), salt=params[11:], checksum=h64.decode_bytes(digest) if digest else None, ) #=================================================================== # formatting #=================================================================== def to_string(self): ident = self.ident if ident == IDENT_SCRYPT: return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % ( self.rounds, self.block_size, self.parallelism, bascii_to_str(b64s_encode(self.salt)), bascii_to_str(b64s_encode(self.checksum)), ) else: assert ident == IDENT_7 salt = self.salt try: salt.decode("ascii") except UnicodeDecodeError: raise suppress_cause(NotImplementedError("scrypt $7$ hashes dont support non-ascii salts")) return bascii_to_str(b"".join([ b"$7$", h64.encode_int6(self.rounds), h64.encode_int30(self.block_size), h64.encode_int30(self.parallelism), self.salt, b"$", h64.encode_bytes(self.checksum) ])) #=================================================================== # init #=================================================================== def __init__(self, block_size=None, **kwds): super(scrypt, self).__init__(**kwds) # init block size if block_size is None: assert uh.validate_default_value(self, self.block_size, self._norm_block_size, param="block_size") else: self.block_size = self._norm_block_size(block_size) # NOTE: if hash contains invalid complex constraint, relying on error # being raised by scrypt call in _calc_checksum() @classmethod def _norm_block_size(cls, block_size, relaxed=False): return uh.norm_integer(cls, block_size, min=1, param="block_size", relaxed=relaxed) def _generate_salt(self): salt = super(scrypt, self)._generate_salt() if self.ident == IDENT_7: # this format doesn't support non-ascii salts. # as workaround, we take raw bytes, encoded to base64 salt = b64s_encode(salt) return salt #=================================================================== # backend configuration # NOTE: this following HasManyBackends' API, but provides it's own implementation, # which actually switches the backend that 'passlib.crypto.scrypt.scrypt()' uses. #=================================================================== @classproperty def backends(cls): return _scrypt.backend_values @classmethod def get_backend(cls): return _scrypt.backend @classmethod def has_backend(cls, name="any"): try: cls.set_backend(name, dryrun=True) return True except uh.exc.MissingBackendError: return False @classmethod def set_backend(cls, name="any", dryrun=False): _scrypt._set_backend(name, dryrun=dryrun) #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): secret = to_bytes(secret, param="secret") return _scrypt.scrypt(secret, self.salt, n=(1 << self.rounds), r=self.block_size, p=self.parallelism, keylen=self.checksum_size) #=================================================================== # hash migration #=================================================================== def _calc_needs_update(self, **kwds): """ mark hash as needing update if rounds is outside desired bounds. """ # XXX: for now, marking all hashes which don't have matching block_size setting if self.block_size != type(self).block_size: return True return super(scrypt, self)._calc_needs_update(**kwds) #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/oracle.py0000644000175000017500000001504313015205366021363 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__) # site # pkg from passlib.utils import to_unicode, xor_bytes from passlib.utils.compat import irange, u, \ uascii_to_str, unicode, str_to_uascii from passlib.crypto.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.hash`, :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.using` method accepts 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 #--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 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.7.1/passlib/handlers/scram.py0000644000175000017500000005401313015205366021223 0ustar biscuitbiscuit00000000000000"""passlib.handlers.scram - hash for SCRAM credential storage""" #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import consteq, saslprep, to_native_str, splitcomma from passlib.utils.binary import ab64_decode, ab64_encode from passlib.utils.compat import bascii_to_str, iteritems, u, native_string_types from passlib.crypto.digest import pbkdf2_hmac, 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.using` method accepts 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 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.crypto.digest.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; possible values are ``"iana"`` or ``"hashlib"``. :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") # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8, # and handle normalizing alg name. return pbkdf2_hmac(alg, saslprep(password), salt, rounds) #=================================================================== # 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): salt = bascii_to_str(ab64_encode(self.salt)) chkmap = self.checksum chk_str = ",".join( "%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg]))) for alg in self.algs ) return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str) #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, default_algs=None, algs=None, **kwds): # parse aliases if algs is not None: assert default_algs is None default_algs = algs # create subclass subcls = super(scram, cls).using(**kwds) # fill in algs if default_algs is not None: subcls.default_algs = cls._norm_algs(default_algs) return subcls #=================================================================== # init #=================================================================== def __init__(self, algs=None, **kwds): super(scram, self).__init__(**kwds) # init algs digest_map = self.checksum if algs is not None: if digest_map is not None: raise RuntimeError("checksum & algs kwds are mutually exclusive") algs = self._norm_algs(algs) elif digest_map is not None: # derive algs list from digest map (if present). algs = self._norm_algs(digest_map.keys()) elif self.use_defaults: algs = list(self.default_algs) assert self._norm_algs(algs) == algs, "invalid default algs: %r" % (algs,) else: raise TypeError("no algs list specified") self.algs = algs def _norm_checksum(self, checksum, relaxed=False): if not isinstance(checksum, dict): raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum") 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 @classmethod def _norm_algs(cls, algs): """normalize algs parameter""" 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 #=================================================================== # migration #=================================================================== def _calc_needs_update(self, **kwds): # marks hashes as deprecated if they don't include at least all default_algs. # XXX: should we deprecate if they aren't exactly the same, # to permit removing legacy hashes? if not set(self.algs).issuperset(self.default_algs): return True # hand off to base implementation return super(scram, self)._calc_needs_update(**kwds) #=================================================================== # digest methods #=================================================================== 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.7.1/passlib/handlers/roundup.py0000644000175000017500000000223212214647076021616 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.7.1/passlib/handlers/des_crypt.py0000644000175000017500000005337213015205366022121 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 safe_crypt, test_crypt, to_unicode from passlib.utils.binary import h64, h64big from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause from passlib.crypto.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. # 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. salt_value = h64.decode_int12(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): """convert 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 salt_value = h64.decode_int24(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.TruncateMixin, 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.using` method accepts 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]``. :param bool truncate_error: By default, des_crypt will silently truncate passwords larger than 8 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 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 ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "des_crypt" setting_kwds = ("salt", "truncate_error") #-------------------- # GenericHandler #-------------------- checksum_chars = uh.HASH64_CHARS checksum_size = 11 #-------------------- # HasSalt #-------------------- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #-------------------- # TruncateMixin #-------------------- truncate_size = 8 #=================================================================== # 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) return uascii_to_str(hash) #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(secret) return self._calc_checksum_backend(secret) #=================================================================== # backend #=================================================================== backends = ("os_crypt", "builtin") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- @classmethod def _load_backend_os_crypt(cls): if test_crypt("test", 'abgOeLfPimXQo'): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False def _calc_checksum_os_crypt(self, secret): # NOTE: we let safe_crypt() encode 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: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True def _calc_checksum_builtin(self, secret): return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii") #=================================================================== # 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.using` method accepts 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:`hash` 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) return uascii_to_str(hash) #=================================================================== # validation #=================================================================== # NOTE: keeping this flag for admin/choose_rounds.py script. # want to eventually expose rounds logic to that script in better way. _avoid_even_rounds = True @classmethod def using(cls, **kwds): subcls = super(bsdi_crypt, cls).using(**kwds) if not subcls.default_rounds & 1: # issue warning if caller set an even 'rounds' value. warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys", uh.exc.PasslibSecurityWarning) return subcls @classmethod def _generate_rounds(cls): rounds = super(bsdi_crypt, cls)._generate_rounds() # ensure autogenerated rounds are always odd # NOTE: doing this even for default_rounds so needs_update() doesn't get # caught in a loop. # FIXME: this technically might generate a rounds value 1 larger # than the requested upper bound - but better to err on side of safety. return rounds|1 #=================================================================== # migration #=================================================================== def _calc_needs_update(self, **kwds): # mark bsdi_crypt hashes as deprecated if they have even rounds. if not self.rounds & 1: return True # hand off to base implementation return super(bsdi_crypt, self)._calc_needs_update(**kwds) #=================================================================== # backends #=================================================================== backends = ("os_crypt", "builtin") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- @classmethod def _load_backend_os_crypt(cls): if test_crypt("test", '_/...lLDAxARksGCHin.'): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False 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: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True def _calc_checksum_builtin(self, secret): return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") #=================================================================== # 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.using` method accepts 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) return uascii_to_str(hash) def _norm_checksum(self, checksum, relaxed=False): checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed) if len(checksum) % 11: raise uh.exc.InvalidHashError(self) return checksum #=================================================================== # 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.TruncateMixin, 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.using` method accepts 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]``. :param bool truncate_error: By default, crypt16 will silently truncate passwords larger than 16 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 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 ``salt`` strings that are too long. .. versionadded:: 1.6 """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "crypt16" setting_kwds = ("salt", "truncate_error") #-------------------- # GenericHandler #-------------------- checksum_size = 22 checksum_chars = uh.HASH64_CHARS #-------------------- # HasSalt #-------------------- min_salt_size = max_salt_size = 2 salt_chars = uh.HASH64_CHARS #-------------------- # TruncateMixin #-------------------- truncate_size = 16 #=================================================================== # 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) return uascii_to_str(hash) #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(secret) # parse salt value try: salt_value = h64.decode_int12(self.salt.encode("ascii")) except ValueError: # pragma: no cover - caught by class raise suppress_cause(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.7.1/passlib/handlers/sha1_crypt.py0000644000175000017500000001330013015205366022165 0ustar biscuitbiscuit00000000000000"""passlib.handlers.sha1_crypt """ #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import safe_crypt, test_crypt from passlib.utils.binary import h64 from passlib.utils.compat import u, unicode, irange from passlib.crypto.digest import compile_hmac import passlib.utils.handlers as uh # local __all__ = [ ] #============================================================================= # sha1-crypt #============================================================================= _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.using` method accepts 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 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") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- @classmethod def _load_backend_os_crypt(cls): if test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHim' 'ExLaiSFlGkAe'): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False 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: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True 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. keyed_hmac = compile_hmac("sha1", secret) for _ in irange(rounds): result = keyed_hmac(result) 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, ] #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/argon2.py0000644000175000017500000007642113043410350021305 0ustar biscuitbiscuit00000000000000"""passlib.handlers.argon2 -- argon2 password hash wrapper References ========== * argon2 - home: https://github.com/P-H-C/phc-winner-argon2 - whitepaper: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf * argon2 cffi wrapper - pypi: https://pypi.python.org/pypi/argon2_cffi - home: https://github.com/hynek/argon2_cffi * argon2 pure python - pypi: https://pypi.python.org/pypi/argon2pure - home: https://github.com/bwesterb/argon2pure """ #============================================================================= # imports #============================================================================= from __future__ import with_statement, absolute_import # core import logging log = logging.getLogger(__name__) import re import types from warnings import warn # site _argon2_cffi = None # loaded below _argon2pure = None # dynamically imported by _load_backend_argon2pure() # pkg from passlib import exc from passlib.crypto.digest import MAX_UINT32 from passlib.utils import to_bytes from passlib.utils.binary import b64s_encode, b64s_decode from passlib.utils.compat import u, unicode, bascii_to_str import passlib.utils.handlers as uh # local __all__ = [ "argon2", ] #============================================================================= # import argon2 package (https://pypi.python.org/pypi/argon2_cffi) #============================================================================= # import package try: import argon2 as _argon2_cffi except ImportError: _argon2_cffi = None # get default settings for hasher _PasswordHasher = getattr(_argon2_cffi, "PasswordHasher", None) if _PasswordHasher: # we have argon2_cffi >= 16.0, use their default hasher settings _default_settings = _PasswordHasher() _default_version = _argon2_cffi.low_level.ARGON2_VERSION else: # use these as our fallback settings (for no backend, or argon2pure) class _default_settings: """ dummy object to use as source of defaults when argon2 mod not present. synced w/ argon2 16.1 as of 2016-6-16 """ time_cost = 2 memory_cost = 512 parallelism = 2 salt_len = 16 hash_len = 16 _default_version = 0x13 #============================================================================= # handler #============================================================================= class _Argon2Common(uh.SubclassBackendMixin, uh.ParallelismMixin, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """ Base class which implements brunt of Argon2 code. This is then subclassed by the various backends, to override w/ backend-specific methods. When a backend is loaded, the bases of the 'argon2' class proper are modified to prepend the correct backend-specific subclass. """ #=================================================================== # class attrs #=================================================================== #------------------------ # PasswordHash #------------------------ name = "argon2" setting_kwds = ("salt", "salt_size", "salt_len", # 'salt_size' alias for compat w/ argon2 package "rounds", "time_cost", # 'rounds' alias for compat w/ argon2 package "memory_cost", "parallelism", "digest_size", "hash_len", # 'digest_size' alias for compat w/ argon2 package ) # TODO: could support the optional 'data' parameter, # but need to research the uses, what a more descriptive name would be, # and deal w/ fact that argon2_cffi 16.1 doesn't currently support it. # (argon2_pure does though) #------------------------ # GenericHandler #------------------------ ident = u("$argon2i") checksum_size = _default_settings.hash_len # NOTE: from_string() relies on the ordering of these... ident_values = (u("$argon2i$"), u("$argon2d$")) #------------------------ # HasSalt #------------------------ default_salt_size = _default_settings.salt_len min_salt_size = 8 max_salt_size = MAX_UINT32 #------------------------ # HasRounds # TODO: once rounds limit logic is factored out, # make 'rounds' and 'cost' an alias for 'time_cost' #------------------------ default_rounds = _default_settings.time_cost min_rounds = 1 max_rounds = MAX_UINT32 rounds_cost = "linear" #------------------------ # ParalleismMixin #------------------------ max_parallelism = (1 << 24) - 1 # from argon2.h / ARGON2_MAX_LANES #------------------------ # custom #------------------------ #: max version support #: NOTE: this is dependant on the backend, and initialized/modified by set_backend() max_version = _default_version #: minimum version before needs_update() marks the hash; if None, defaults to max_version min_desired_version = None #: minimum valid memory_cost min_memory_cost = 8 # from argon2.h / ARGON2_MIN_MEMORY #: maximum number of threads (-1=unlimited); #: number of threads used by .hash() will be min(parallelism, max_threads) max_threads = -1 #: global flag signalling argon2pure backend to use threads #: rather than subprocesses. pure_use_threads = False #=================================================================== # instance attrs #=================================================================== #: parallelism setting -- class value controls the default parallelism = _default_settings.parallelism #: hash version (int) #: NOTE: this is modified by set_backend() version = _default_version #: memory cost -- class value controls the default memory_cost = _default_settings.memory_cost #: flag indicating a Type D hash type_d = False #: optional secret data data = None #=================================================================== # variant constructor #=================================================================== @classmethod def using(cls, memory_cost=None, salt_len=None, time_cost=None, digest_size=None, checksum_size=None, hash_len=None, max_threads=None, **kwds): # support aliases which match argon2 naming convention if time_cost is not None: if "rounds" in kwds: raise TypeError("'time_cost' and 'rounds' are mutually exclusive") kwds['rounds'] = time_cost if salt_len is not None: if "salt_size" in kwds: raise TypeError("'salt_len' and 'salt_size' are mutually exclusive") kwds['salt_size'] = salt_len if hash_len is not None: if digest_size is not None: raise TypeError("'hash_len' and 'digest_size' are mutually exclusive") digest_size = hash_len if checksum_size is not None: if digest_size is not None: raise TypeError("'checksum_size' and 'digest_size' are mutually exclusive") digest_size = checksum_size # create variant subcls = super(_Argon2Common, cls).using(**kwds) # set checksum size relaxed = kwds.get("relaxed") if digest_size is not None: if isinstance(digest_size, uh.native_string_types): digest_size = int(digest_size) # NOTE: this isn't *really* digest size minimum, but want to enforce secure minimum. subcls.checksum_size = uh.norm_integer(subcls, digest_size, min=16, max=MAX_UINT32, param="digest_size", relaxed=relaxed) # set memory cost if memory_cost is not None: if isinstance(memory_cost, uh.native_string_types): memory_cost = int(memory_cost) subcls.memory_cost = subcls._norm_memory_cost(memory_cost, relaxed=relaxed) # validate constraints subcls._validate_constraints(subcls.memory_cost, subcls.parallelism) # set max threads if max_threads is not None: if isinstance(max_threads, uh.native_string_types): max_threads = int(max_threads) if max_threads < 1 and max_threads != -1: raise ValueError("max_threads (%d) must be -1 (unlimited), or at least 1." % (max_threads,)) subcls.max_threads = max_threads return subcls @classmethod def _validate_constraints(cls, memory_cost, parallelism): # NOTE: this is used by class & instance, hence passing in via arguments. # could switch and make this a hybrid method. min_memory_cost = 8 * parallelism if memory_cost < min_memory_cost: raise ValueError("%s: memory_cost (%d) is too low, must be at least " "8 * parallelism (8 * %d = %d)" % (cls.name, memory_cost, parallelism, min_memory_cost)) #=================================================================== # public api #=================================================================== @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) return hash.startswith(cls.ident_values) # hash(), verify(), genhash() -- implemented by backend subclass #=================================================================== # hash parsing / rendering #=================================================================== # info taken from source of decode_string() function in # # # hash format: # $argon2[$v=]$m=,t=,p=[,keyid=][,data=][$[$]] # # NOTE: as of 2016-6-17, the official source (above) lists the "keyid" param in the comments, # but the actual source of decode_string & encode_string don't mention it at all. # we're supporting parsing it, but throw NotImplementedError if encountered. # # sample hashes: # v1.0: '$argon2i$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ' # v1.3: '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ' #: regex to parse argon hash _hash_regex = re.compile(br""" ^ \$argon2(?P[id])\$ (?: v=(?P\d+) \$ )? m=(?P\d+) , t=(?P\d+) , p=(?P\d+) (?: ,keyid=(?P[^,$]+) )? (?: ,data=(?P[^,$]+) )? (?: \$ (?P[^$]+) (?: \$ (?P.+) )? )? $ """, re.X) @classmethod def from_string(cls, hash): # NOTE: assuming hash will be unicode, or use ascii-compatible encoding. if isinstance(hash, unicode): hash = hash.encode("utf-8") if not isinstance(hash, bytes): raise exc.ExpectedStringError(hash, "hash") m = cls._hash_regex.match(hash) if not m: raise exc.MalformedHashError(cls) type, version, memory_cost, time_cost, parallelism, keyid, data, salt, digest = \ m.group("type", "version", "memory_cost", "time_cost", "parallelism", "keyid", "data", "salt", "digest") assert type in [b"i", b"d"], "unexpected type code: %r" % (type,) if keyid: raise NotImplementedError("argon2 'keyid' parameter not supported") return cls( type_d=(type == b"d"), version=int(version) if version else 0x10, memory_cost=int(memory_cost), rounds=int(time_cost), parallelism=int(parallelism), salt=b64s_decode(salt) if salt else None, data=b64s_decode(data) if data else None, checksum=b64s_decode(digest) if digest else None, ) def to_string(self): ident = str(self.ident_values[self.type_d]) version = self.version if version == 0x10: vstr = "" else: vstr = "v=%d$" % version data = self.data if data: kdstr = ",data=" + bascii_to_str(b64s_encode(self.data)) else: kdstr = "" # NOTE: 'keyid' param currently not supported return "%s%sm=%d,t=%d,p=%d%s$%s$%s" % (ident, vstr, self.memory_cost, self.rounds, self.parallelism, kdstr, bascii_to_str(b64s_encode(self.salt)), bascii_to_str(b64s_encode(self.checksum))) #=================================================================== # init #=================================================================== def __init__(self, type_d=False, version=None, memory_cost=None, data=None, **kwds): # TODO: factor out variable checksum size support into a mixin. # set checksum size to specific value before _norm_checksum() is called checksum = kwds.get("checksum") if checksum is not None: self.checksum_size = len(checksum) # call parent super(_Argon2Common, self).__init__(**kwds) # init type # NOTE: we don't support *generating* type I hashes, but do support verifying them. self.type_d = type_d # init version if version is None: assert uh.validate_default_value(self, self.version, self._norm_version, param="version") else: self.version = self._norm_version(version) # init memory cost if memory_cost is None: assert uh.validate_default_value(self, self.memory_cost, self._norm_memory_cost, param="memory_cost") else: self.memory_cost = self._norm_memory_cost(memory_cost) # init data if data is None: assert self.data is None else: if not isinstance(data, bytes): raise uh.exc.ExpectedTypeError(data, "bytes", "data") self.data = data #------------------------------------------------------------------- # parameter guards #------------------------------------------------------------------- @classmethod def _norm_version(cls, version): if not isinstance(version, uh.int_types): raise uh.exc.ExpectedTypeError(version, "integer", "version") # minimum valid version if version < 0x13 and version != 0x10: raise ValueError("invalid argon2 hash version: %d" % (version,)) # check this isn't past backend's max version backend = cls.get_backend() if version > cls.max_version: raise ValueError("%s: hash version 0x%X not supported by %r backend " "(max version is 0x%X); try updating or switching backends" % (cls.name, version, backend, cls.max_version)) return version @classmethod def _norm_memory_cost(cls, memory_cost, relaxed=False): return uh.norm_integer(cls, memory_cost, min=cls.min_memory_cost, param="memory_cost", relaxed=relaxed) #=================================================================== # digest calculation #=================================================================== # NOTE: _calc_checksum implemented by backend subclass #=================================================================== # hash migration #=================================================================== def _calc_needs_update(self, **kwds): cls = type(self) if self.type_d: # type 'd' hashes shouldn't be used for passwords. return True minver = cls.min_desired_version if minver is None or minver > cls.max_version: minver = cls.max_version if self.version < minver: # version is too old. return True if self.memory_cost != cls.memory_cost: return True if self.checksum_size != cls.checksum_size: return True return super(_Argon2Common, self)._calc_needs_update(**kwds) #=================================================================== # backend loading #=================================================================== _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install argon2_cffi')" @classmethod def _finalize_backend_mixin(mixin_cls, name, dryrun): """ helper called by from backend mixin classes' _load_backend_mixin() -- invoked after backend imports have been loaded, and performs feature detection & testing common to all backends. """ max_version = mixin_cls.max_version assert isinstance(max_version, int) and max_version >= 0x10 if max_version < 0x13: warn("%r doesn't support argon2 v1.3, and should be upgraded" % name, uh.exc.PasslibSecurityWarning) return True @classmethod def _adapt_backend_error(cls, err, hash=None, self=None): """ internal helper invoked when backend has hash/verification error; used to adapt to passlib message. """ backend = cls.get_backend() # parse hash to throw error if format was invalid, parameter out of range, etc. if self is None and hash is not None: self = cls.from_string(hash) # check constraints on parsed object # XXX: could move this to __init__, but not needed by needs_update calls if self is not None: self._validate_constraints(self.memory_cost, self.parallelism) # as of cffi 16.1, lacks support in hash_secret(), so genhash() will get here. # as of cffi 16.2, support removed from verify_secret() as well. if backend == "argon2_cffi" and self.data is not None: raise NotImplementedError("argon2_cffi backend doesn't support the 'data' parameter") # fallback to reporting a malformed hash text = str(err) if text not in [ "Decoding failed" # argon2_cffi's default message ]: reason = "%s reported: %s: hash=%r" % (backend, text, hash) else: reason = repr(hash) raise exc.MalformedHashError(cls, reason=reason) #=================================================================== # eoc #=================================================================== #----------------------------------------------------------------------- # stub backend #----------------------------------------------------------------------- class _NoBackend(_Argon2Common): """ mixin used before any backend has been loaded. contains stubs that force loading of one of the available backends. """ #=================================================================== # primary methods #=================================================================== @classmethod def hash(cls, secret): cls._stub_requires_backend() return cls.hash(secret) @classmethod def verify(cls, secret, hash): cls._stub_requires_backend() return cls.verify(secret, hash) @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config): cls._stub_requires_backend() return cls.genhash(secret, config) #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): # NOTE: since argon2_cffi takes care of rendering hash, # _calc_checksum() is only used by the argon2pure backend. self._stub_requires_backend() # NOTE: have to use super() here so that we don't recursively # call subclass's wrapped _calc_checksum return super(argon2, self)._calc_checksum(secret) #=================================================================== # eoc #=================================================================== #----------------------------------------------------------------------- # argon2_cffi backend #----------------------------------------------------------------------- class _CffiBackend(_Argon2Common): """ argon2_cffi backend """ #=================================================================== # backend loading #=================================================================== @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): # we automatically import this at top, so just grab info if _argon2_cffi is None: return False max_version = _argon2_cffi.low_level.ARGON2_VERSION log.debug("detected 'argon2_cffi' backend, version %r, with support for 0x%x argon2 hashes", _argon2_cffi.__version__, max_version) mixin_cls.version = mixin_cls.max_version = max_version return mixin_cls._finalize_backend_mixin(name, dryrun) #=================================================================== # primary methods #=================================================================== @classmethod def hash(cls, secret): # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. uh.validate_secret(secret) secret = to_bytes(secret, "utf-8") # XXX: doesn't seem to be a way to make this honor max_threads try: return bascii_to_str(_argon2_cffi.low_level.hash_secret( type=_argon2_cffi.low_level.Type.I, memory_cost=cls.memory_cost, time_cost=cls.default_rounds, parallelism=cls.parallelism, salt=to_bytes(cls._generate_salt()), hash_len=cls.checksum_size, secret=secret, )) except _argon2_cffi.exceptions.HashingError as err: raise cls._adapt_backend_error(err) @classmethod def verify(cls, secret, hash): # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. uh.validate_secret(secret) secret = to_bytes(secret, "utf-8") hash = to_bytes(hash, "ascii") if hash.startswith(b"$argon2d$"): type = _argon2_cffi.low_level.Type.D else: type = _argon2_cffi.low_level.Type.I # XXX: doesn't seem to be a way to make this honor max_threads try: result = _argon2_cffi.low_level.verify_secret(hash, secret, type) assert result is True return True except _argon2_cffi.exceptions.VerifyMismatchError: return False except _argon2_cffi.exceptions.VerificationError as err: raise cls._adapt_backend_error(err, hash=hash) # NOTE: deprecated, will be removed in 2.0 @classmethod def genhash(cls, secret, config): # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. uh.validate_secret(secret) secret = to_bytes(secret, "utf-8") self = cls.from_string(config) if self.type_d: type = _argon2_cffi.low_level.Type.D else: type = _argon2_cffi.low_level.Type.I # XXX: doesn't seem to be a way to make this honor max_threads try: result = bascii_to_str(_argon2_cffi.low_level.hash_secret( type=type, memory_cost=self.memory_cost, time_cost=self.rounds, parallelism=self.parallelism, salt=to_bytes(self.salt), hash_len=self.checksum_size, secret=secret, version=self.version, )) except _argon2_cffi.exceptions.HashingError as err: raise cls._adapt_backend_error(err, hash=config) if self.version == 0x10: # workaround: argon2 0x13 always returns "v=" segment, even for 0x10 hashes result = result.replace("$v=16$", "$") return result #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): raise AssertionError("shouldn't be called under argon2_cffi backend") #=================================================================== # eoc #=================================================================== #----------------------------------------------------------------------- # argon2pure backend #----------------------------------------------------------------------- class _PureBackend(_Argon2Common): """ argon2pure backend """ #=================================================================== # backend loading #=================================================================== @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): # import argon2pure global _argon2pure try: import argon2pure as _argon2pure except ImportError: return False # get default / max supported version -- added in v1.2.2 try: from argon2pure import ARGON2_DEFAULT_VERSION as max_version except ImportError: log.warning("detected 'argon2pure' backend, but package is too old " "(passlib requires argon2pure >= 1.2.3)") return False log.debug("detected 'argon2pure' backend, with support for 0x%x argon2 hashes", max_version) if not dryrun: warn("Using argon2pure backend, which is 100x+ slower than is required " "for adequate security. Installing argon2_cffi (via 'pip install argon2_cffi') " "is strongly recommended", exc.PasslibSecurityWarning) mixin_cls.version = mixin_cls.max_version = max_version return mixin_cls._finalize_backend_mixin(name, dryrun) #=================================================================== # primary methods #=================================================================== # NOTE: this backend uses default .hash() & .verify() implementations. #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9. uh.validate_secret(secret) secret = to_bytes(secret, "utf-8") if self.type_d: type = _argon2pure.ARGON2D else: type = _argon2pure.ARGON2I kwds = dict( password=secret, salt=self.salt, time_cost=self.rounds, memory_cost=self.memory_cost, parallelism=self.parallelism, tag_length=self.checksum_size, type_code=type, version=self.version, ) if self.max_threads > 0: kwds['threads'] = self.max_threads if self.pure_use_threads: kwds['use_threads'] = True if self.data: kwds['associated_data'] = self.data # NOTE: should return raw bytes # NOTE: this may raise _argon2pure.Argon2ParameterError, # but it if does that, there's a bug in our own parameter checking code. try: return _argon2pure.argon2(**kwds) except _argon2pure.Argon2Error as err: raise self._adapt_backend_error(err, self=self) #=================================================================== # eoc #=================================================================== class argon2(_NoBackend, _Argon2Common): """ This class implements the Argon2 password hash [#argon2-home]_, and follows the :ref:`password-hash-api`. (This class only supports generating "Type I" argon2 hashes). Argon2 supports a variable-length salt, and variable time & memory cost, and a number of other configurable parameters. The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords: :type salt: str :param salt: Optional salt string. If specified, the length must be between 0-1024 bytes. If not specified, one will be auto-generated (this is recommended). :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. :type rounds: int :param rounds: Optional number of rounds to use. This corresponds linearly to the amount of time hashing will take. :type time_cost: int :param time_cost: An alias for **rounds**, for compatibility with underlying argon2 library. :param int memory_cost: Defines the memory usage in kibibytes. This corresponds linearly to the amount of memory hashing will take. :param int parallelism: Defines the parallelization factor. *NOTE: this will affect the resulting hash value.* :param int digest_size: Length of the digest in bytes. :param int max_threads: Maximum number of threads that will be used. -1 means unlimited; otherwise hashing will use ``min(parallelism, max_threads)`` threads. .. note:: This option is currently only honored by the argon2pure backend. :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. .. todo:: * Support configurable threading limits. """ #============================================================================= # backend #============================================================================= # NOTE: the brunt of the argon2 class is implemented in _Argon2Common. # there are then subclass for each backend (e.g. _PureBackend), # these are dynamically prepended to this class's bases # in order to load the appropriate backend. #: list of potential backends backends = ("argon2_cffi", "argon2pure") #: flag that this class's bases should be modified by SubclassBackendMixin _backend_mixin_target = True #: map of backend -> mixin class, used by _get_backend_loader() _backend_mixin_map = { None: _NoBackend, "argon2_cffi": _CffiBackend, "argon2pure": _PureBackend, } #============================================================================= # #============================================================================= #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/bcrypt.py0000644000175000017500000012251613033503240021415 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 _bcrypt = None # dynamically imported by _load_backend_bcrypt() _pybcrypt = None # dynamically imported by _load_backend_pybcrypt() _bcryptor = None # dynamically imported by _load_backend_bcryptor() # pkg _builtin_bcrypt = None # dynamically imported by _load_backend_builtin() from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \ rng, getrandstr, test_crypt, to_unicode from passlib.utils.binary import bcrypt64 from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii import passlib.utils.handlers as uh # local __all__ = [ "bcrypt", ] #============================================================================= # support funcs & constants #============================================================================= IDENT_2 = u("$2$") IDENT_2A = u("$2a$") IDENT_2X = u("$2x$") IDENT_2Y = u("$2y$") IDENT_2B = u("$2b$") _BNULL = b'\x00' # reference hash of "test", used in various self-checks TEST_HASH_2A = b"$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK" 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 #============================================================================= # backend mixins #============================================================================= class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler): """ Base class which implements brunt of BCrypt code. This is then subclassed by the various backends, to override w/ backend-specific methods. When a backend is loaded, the bases of the 'bcrypt' class proper are modified to prepend the correct backend-specific subclass. """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "bcrypt" setting_kwds = ("salt", "rounds", "ident", "truncate_error") #-------------------- # GenericHandler #-------------------- checksum_size = 31 checksum_chars = bcrypt64.charmap #-------------------- # HasManyIdents #-------------------- default_ident = IDENT_2B 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< class # NOTE: set_backend() will execute the ._load_backend_mixin() # of the matching mixin class, which will handle backend detection # appended to HasManyBackends' "no backends available" error message _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')" @classmethod def _finalize_backend_mixin(mixin_cls, backend, dryrun): """ helper called by from backend mixin classes' _load_backend_mixin() -- invoked after backend imports have been loaded, and performs feature detection & testing common to all backends. """ #---------------------------------------------------------------- # setup helpers #---------------------------------------------------------------- assert mixin_cls is bcrypt._backend_mixin_map[backend], \ "_configure_workarounds() invoked from wrong class" if mixin_cls._workrounds_initialized: return True verify = mixin_cls.verify err_types = (ValueError,) if _bcryptor: err_types += (_bcryptor.engine.SaltError,) def safe_verify(secret, hash): """verify() wrapper which traps 'unknown identifier' errors""" try: return verify(secret, hash) except err_types: # backends without support for given ident will throw various # errors about unrecognized version: # pybcrypt, bcrypt -- raises ValueError # bcryptor -- raises bcryptor.engine.SaltError return NotImplemented except AssertionError as err: # _calc_checksum() code may also throw AssertionError # if correct hash isn't returned (e.g. 2y hash converted to 2b, # such as happens with bcrypt 3.0.0) log.debug("trapped unexpected response from %r backend: verify(%r, %r):", backend, secret, hash, exc_info=True) return NotImplemented def assert_lacks_8bit_bug(ident): """ helper to check for cryptblowfish 8bit bug (fixed in 2y/2b); even though it's not known to be present in any of passlib's backends. this is treated as FATAL, because it can easily result in seriously malformed hashes, and we can't correct for it ourselves. test cases from reference hash is the incorrectly generated $2x$ hash taken from above url """ secret = b"\xA3" bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e" if verify(secret, bug_hash): # NOTE: this only EVER be observed in 2a hashes, # 2y/2b hashes should have fixed the bug. # (but we check w/ them anyways). 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) # if it doesn't have wraparound bug, make sure it *does* handle things # correctly -- or we're in some weird third case. correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq" if not verify(secret, correct_hash): raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident)) def detect_wrap_bug(ident): """ 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)' """ # check if it exhibits wraparound bug secret = (b"0123456789"*26)[:255] bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6" if verify(secret, bug_hash): return True # if it doesn't have wraparound bug, make sure it *does* handle things # correctly -- or we're in some weird third case. correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi" if not verify(secret, correct_hash): raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident)) return False def assert_lacks_wrap_bug(ident): if not detect_wrap_bug(ident): return # should only see in 2a, later idents should NEVER exhibit this bug: # * 2y implementations should have been free of it # * 2b was what (supposedly) fixed it raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident)) #---------------------------------------------------------------- # check for old 20 support #---------------------------------------------------------------- test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S" result = safe_verify("test", test_hash_20) if not result: raise RuntimeError("%s incorrectly rejected $2$ hash" % backend) elif result is NotImplemented: mixin_cls._lacks_20_support = True log.debug("%r backend lacks $2$ support, enabling workaround", backend) #---------------------------------------------------------------- # check for 2a support #---------------------------------------------------------------- result = safe_verify("test", TEST_HASH_2A) if not result: raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend) elif result is NotImplemented: # 2a support is required, and should always be present raise RuntimeError("%s lacks support for $2a$ hashes" % backend) else: assert_lacks_8bit_bug(IDENT_2A) if detect_wrap_bug(IDENT_2A): 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 " "(enabling workaround for now)." % backend, uh.exc.PasslibSecurityWarning) mixin_cls._has_2a_wraparound_bug = True #---------------------------------------------------------------- # check for 2y support #---------------------------------------------------------------- test_hash_2y = TEST_HASH_2A.replace(b"2a", b"2y") result = safe_verify("test", test_hash_2y) if not result: raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend) elif result is NotImplemented: mixin_cls._lacks_2y_support = True log.debug("%r backend lacks $2y$ support, enabling workaround", backend) else: # NOTE: Not using this as fallback candidate, # lacks wide enough support across implementations. assert_lacks_8bit_bug(IDENT_2Y) assert_lacks_wrap_bug(IDENT_2Y) #---------------------------------------------------------------- # TODO: check for 2x support #---------------------------------------------------------------- #---------------------------------------------------------------- # check for 2b support #---------------------------------------------------------------- test_hash_2b = TEST_HASH_2A.replace(b"2a", b"2b") result = safe_verify("test", test_hash_2b) if not result: raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend) elif result is NotImplemented: mixin_cls._lacks_2b_support = True log.debug("%r backend lacks $2b$ support, enabling workaround", backend) else: mixin_cls._fallback_ident = IDENT_2B assert_lacks_8bit_bug(IDENT_2B) assert_lacks_wrap_bug(IDENT_2B) # set flag so we don't have to run this again mixin_cls._workrounds_initialized = True return True #=================================================================== # digest calculation #=================================================================== # _calc_checksum() defined by backends def _prepare_digest_args(self, secret): """ common helper for backends to implement _calc_checksum(). takes in secret, returns (secret, ident) pair, """ return self._norm_digest_args(secret, self.ident, new=self.use_defaults) @classmethod def _norm_digest_args(cls, secret, ident, new=False): # make sure secret is unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") # check max secret size uh.validate_secret(secret) # check for truncation (during .hash() calls only) if new: cls._check_truncate_policy(secret) # 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(cls) # TODO: figure out way to skip these tests when not needed... # protect from wraparound bug by truncating secret before handing it to the backend. # bcrypt only uses first 72 bytes anyways. # NOTE: not needed for 2y/2b, but might use 2a as fallback for them. if cls._has_2a_wraparound_bug and len(secret) >= 255: secret = secret[:72] # special case handling for variants (ordered most common first) if ident == IDENT_2A: # nothing needs to be done. pass elif ident == IDENT_2B: if cls._lacks_2b_support: # handle $2b$ hash format even if backend is too old. # have it generate a 2A/2Y digest, then return it as a 2B hash. # 2a-only backend could potentially exhibit wraparound bug -- # but we work around that issue above. ident = cls._fallback_ident elif ident == IDENT_2Y: if cls._lacks_2y_support: # handle $2y$ hash format (not supported by BSDs, being phased out on others) # have it generate a 2A/2B digest, then return it as a 2Y hash. ident = cls._fallback_ident elif ident == IDENT_2: if cls._lacks_20_support: # handle legacy $2$ format (not supported by most backends except BSD os_crypt) # we can fake $2$ behavior using the 2A/2Y/2B algorithm # by repeating the password until it's at least 72 chars in length. if secret: secret = repeat_string(secret, 72) ident = cls._fallback_ident 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) return secret, ident #----------------------------------------------------------------------- # stub backend #----------------------------------------------------------------------- class _NoBackend(_BcryptCommon): """ mixin used before any backend has been loaded. contains stubs that force loading of one of the available backends. """ #=================================================================== # digest calculation #=================================================================== def _calc_checksum(self, secret): self._stub_requires_backend() # NOTE: have to use super() here so that we don't recursively # call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum return super(bcrypt, self)._calc_checksum(secret) #=================================================================== # eoc #=================================================================== #----------------------------------------------------------------------- # bcrypt backend #----------------------------------------------------------------------- class _BcryptBackend(_BcryptCommon): """ backend which uses 'bcrypt' package """ @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): # try to import bcrypt global _bcrypt if _detect_pybcrypt(): # pybcrypt was installed instead return False try: import bcrypt as _bcrypt except ImportError: # pragma: no cover return False try: version = _bcrypt.__about__.__version__ except: log.warning("(trapped) error reading bcrypt version", exc_info=True) version = '' log.debug("detected 'bcrypt' backend, version %r", version) return mixin_cls._finalize_backend_mixin(name, dryrun) # # TODO: would like to implementing verify() directly, # # to skip need for parsing hash strings. # # below method has a few edge cases where it chokes though. # @classmethod # def verify(cls, secret, hash): # if isinstance(hash, unicode): # hash = hash.encode("ascii") # ident = hash[:hash.index(b"$", 1)+1].decode("ascii") # if ident not in cls.ident_values: # raise uh.exc.InvalidHashError(cls) # secret, eff_ident = cls._norm_digest_args(secret, ident) # if eff_ident != ident: # # lacks support for original ident, replace w/ new one. # hash = eff_ident.encode("ascii") + hash[len(ident):] # result = _bcrypt.hashpw(secret, hash) # assert result.startswith(eff_ident) # return consteq(result, hash) def _calc_checksum(self, secret): # bcrypt behavior: # secret must be bytes # config must be ascii bytes # returns ascii bytes secret, ident = self._prepare_digest_args(secret) config = self._get_config(ident) if isinstance(config, unicode): config = config.encode("ascii") hash = _bcrypt.hashpw(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31, \ "config mismatch: %r => %r" % (config, hash) assert isinstance(hash, bytes) return hash[-31:].decode("ascii") #----------------------------------------------------------------------- # bcryptor backend #----------------------------------------------------------------------- class _BcryptorBackend(_BcryptCommon): """ backend which uses 'bcryptor' package """ @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): # try to import bcryptor global _bcryptor try: import bcryptor as _bcryptor except ImportError: # pragma: no cover return False return mixin_cls._finalize_backend_mixin(name, dryrun) def _calc_checksum(self, secret): # bcryptor behavior: # py2: unicode secret/hash encoded as ascii bytes before use, # bytes taken as-is; returns ascii bytes. # py3: not supported secret, ident = self._prepare_digest_args(secret) config = self._get_config(ident) 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:]) #----------------------------------------------------------------------- # pybcrypt backend #----------------------------------------------------------------------- class _PyBcryptBackend(_BcryptCommon): """ backend which uses 'pybcrypt' package """ #: classwide thread lock used for pybcrypt < 0.3 _calc_lock = None @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): # try to import pybcrypt global _pybcrypt if not _detect_pybcrypt(): # not installed, or bcrypt installed instead return False try: import bcrypt as _pybcrypt except ImportError: # pragma: no cover return False # determine pybcrypt version try: version = _pybcrypt._bcrypt.__version__ except: log.warning("(trapped) error reading pybcrypt version", exc_info=True) version = "" log.debug("detected 'pybcrypt' backend, version %r", version) # return calc function based on version vinfo = parse_version(version) or (0, 0) if vinfo < (0, 3): warn("py-bcrypt %s has a major security vulnerability, " "you should upgrade to py-bcrypt 0.3 immediately." % version, uh.exc.PasslibSecurityWarning) if mixin_cls._calc_lock is None: import threading mixin_cls._calc_lock = threading.Lock() mixin_cls._calc_checksum = mixin_cls._calc_checksum_threadsafe.__func__ return mixin_cls._finalize_backend_mixin(name, dryrun) def _calc_checksum_threadsafe(self, secret): # as workaround for pybcrypt < 0.3's concurrency issue, # we wrap everything in a thread lock. as long as bcrypt is only # used through passlib, this should be safe. with self._calc_lock: return self._calc_checksum_raw(secret) def _calc_checksum_raw(self, secret): # 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. secret, ident = self._prepare_digest_args(secret) config = self._get_config(ident) hash = _pybcrypt.hashpw(secret, config) assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) _calc_checksum = _calc_checksum_raw #----------------------------------------------------------------------- # os crypt backend #----------------------------------------------------------------------- class _OsCryptBackend(_BcryptCommon): """ backend which uses :func:`crypt.crypt` """ @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): if not test_crypt("test", TEST_HASH_2A): return False return mixin_cls._finalize_backend_mixin(name, dryrun) def _calc_checksum(self, secret): secret, ident = self._prepare_digest_args(secret) config = self._get_config(ident) 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`.", ) #----------------------------------------------------------------------- # builtin backend #----------------------------------------------------------------------- class _BuiltinBackend(_BcryptCommon): """ backend which uses passlib's pure-python implementation """ @classmethod def _load_backend_mixin(mixin_cls, name, dryrun): from passlib.utils import as_bool if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")): log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT") return False global _builtin_bcrypt from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt return mixin_cls._finalize_backend_mixin(name, dryrun) def _calc_checksum(self, secret): secret, ident = self._prepare_digest_args(secret) chk = _builtin_bcrypt(secret, ident[1:-1], self.salt.encode("ascii"), self.rounds) return chk.decode("ascii") #============================================================================= # handler #============================================================================= class bcrypt(_NoBackend, _BcryptCommon): """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.using` method accepts 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 (``"2b"``) 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 rare security flaws, replaced by 2b. * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation, identical to ``"2b"`` in all but name. * ``"2b"`` - latest revision of the official BCrypt algorithm, current default. :param bool truncate_error: By default, BCrypt will silently truncate passwords larger than 72 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 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. .. versionchanged:: 1.7 Now defaults to ``"2b"`` variant. """ #============================================================================= # backend #============================================================================= # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon. # there are then subclass for each backend (e.g. _PyBcryptBackend), # these are dynamically prepended to this class's bases # in order to load the appropriate backend. #: list of potential backends backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin") #: flag that this class's bases should be modified by SubclassBackendMixin _backend_mixin_target = True #: map of backend -> mixin class, used by _get_backend_loader() _backend_mixin_map = { None: _NoBackend, "bcrypt": _BcryptBackend, "pybcrypt": _PyBcryptBackend, "bcryptor": _BcryptorBackend, "os_crypt": _OsCryptBackend, "builtin": _BuiltinBackend, } #============================================================================= # eoc #============================================================================= #============================================================================= # variants #============================================================================= _UDOLLAR = u("$") # XXX: it might be better to have all the bcrypt variants share a common base class, # and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it. class _wrapped_bcrypt(bcrypt): """ abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share. - bypass backend-loading wrappers for hash() etc - disable truncation support, sha256 wrappers don't need it. """ setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"]) truncate_size = None # XXX: these will be needed if any bcrypt backends directly implement this... # @classmethod # def hash(cls, secret, **kwds): # # bypass bcrypt backend overriding this method # # XXX: would wrapping bcrypt make this easier than subclassing it? # return super(_BcryptCommon, cls).hash(secret, **kwds) # # @classmethod # def verify(cls, secret, hash): # # bypass bcrypt backend overriding this method # return super(_BcryptCommon, cls).verify(secret, hash) # # @classmethod # def genhash(cls, secret, hash): # # bypass bcrypt backend overriding this method # return super(_BcryptCommon, cls).genhash(secret, hash) @classmethod def _check_truncate_policy(cls, secret): # disable check performed by bcrypt(), since this doesn't truncate passwords. pass #============================================================================= # bcrypt sha256 wrapper #============================================================================= class bcrypt_sha256(_wrapped_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.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept all the same optional keywords as the base :class:`bcrypt` hash. .. versionadded:: 1.6.2 .. versionchanged:: 1.7 Now defaults to ``"2b"`` variant. """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "bcrypt_sha256" #-------------------- # GenericHandler #-------------------- # this is locked at 2a/2b for now. ident_values = (IDENT_2A, IDENT_2B) # clone bcrypt's ident aliases so they can be used here as well... ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items() if item[1] in ident_values))(ident_values) default_ident = IDENT_2B #=================================================================== # formatting #=================================================================== # 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 [$](?P2[ab]) ,(?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"), ) _template = u("$bcrypt-sha256$%s,%d$%s$%s") def to_string(self): hash = self._template % (self.ident.strip(_UDOLLAR), self.rounds, self.salt, self.checksum) return uascii_to_str(hash) #=================================================================== # checksum #=================================================================== 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") # NOTE: output of b64encode() uses "+/" altchars, "=" padding chars, # and no leading/trailing whitespace. key = b64encode(sha256(secret).digest()) # hand result off to normal bcrypt algorithm return super(bcrypt_sha256, self)._calc_checksum(key) #=================================================================== # other #=================================================================== # XXX: have _needs_update() mark the $2a$ ones for upgrading? # maybe do that after we switch to hex encoding? #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/md5_crypt.py0000644000175000017500000003257313016611237022032 0ustar biscuitbiscuit00000000000000"""passlib.handlers.md5_crypt - md5-crypt algorithm""" #============================================================================= # imports #============================================================================= # core from hashlib import md5 import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import safe_crypt, test_crypt, repeat_string from passlib.utils.binary import h64 from passlib.utils.compat import 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 hash :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 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.using` method accepts 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") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- @classmethod def _load_backend_os_crypt(cls): if test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/'): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False 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: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True def _calc_checksum_builtin(self, secret): return _raw_md5_crypt(secret, self.salt) #=================================================================== # 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.using` method accepts 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.7.1/passlib/handlers/sha2_crypt.py0000644000175000017500000005126113016611237022175 0ustar biscuitbiscuit00000000000000"""passlib.handlers.sha2_crypt - SHA256-Crypt / SHA512-Crypt""" #============================================================================= # imports #============================================================================= # core import hashlib import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import safe_crypt, test_crypt, \ repeat_string, to_unicode from passlib.utils.binary import h64 from passlib.utils.compat import byte_elem_value, 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 hash :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 #=================================================================== # NOTE: the setup portion of this algorithm scales ~linearly in time # with the size of the password, making it vulnerable to a DOS from # unreasonably large inputs. the following code has some optimizations # which would make things even worse, using O(pwd_len**2) memory # when calculating digest P. # # to mitigate these two issues: 1) this code switches to a # O(pwd_len)-memory algorithm for passwords that are much larger # than average, and 2) Passlib enforces a library-wide max limit on # the size of passwords it will allow, to prevent this algorithm and # others from being DOSed in this way (see passlib.exc.PasswordSizeError # for details). # 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 transpose_map = _512_transpose_map else: hash_const = hashlib.sha256 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 < 96: # this method 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: # this method is slower under python, but uses a fixed amount of memory. 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 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 hash() 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 def _parse_salt(self, salt): # required per SHA2-crypt spec -- truncate config salts rather than throwing error return self._norm_salt(salt, relaxed=self.checksum is None) def _parse_rounds(self, rounds): # required per SHA2-crypt spec -- clip config rounds rather than throwing error return self._norm_rounds(rounds, relaxed=self.checksum is None) @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, ) 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") #--------------------------------------------------------------- # os_crypt backend #--------------------------------------------------------------- #: test hash for OS detection -- provided by subclass _test_hash = None @classmethod def _load_backend_os_crypt(cls): if test_crypt(*cls._test_hash): cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt) return True else: return False 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: # py3's crypt.crypt() can't handle non-utf8 bytes. # fallback to builtin alg, which is always available. return self._calc_checksum_builtin(secret) #--------------------------------------------------------------- # builtin backend #--------------------------------------------------------------- @classmethod def _load_backend_builtin(cls): cls._set_calc_checksum_backend(cls._calc_checksum_builtin) return True def _calc_checksum_builtin(self, secret): return _raw_sha2_crypt(secret, self.salt, self.rounds, self._cdb_use_512) #=================================================================== # 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.using` method accepts 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 #=================================================================== _test_hash = ("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.using` method accepts 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 #=================================================================== _test_hash = ("test", "$6$rounds=1000$test$2M/Lx6Mtobqj" "Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn" "yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG" "I5c7TZauS0") #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/mssql.py0000644000175000017500000002044213015205366021254 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 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.using` method accepts 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 #=================================================================== # 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 # 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.using` method accepts 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 #=================================================================== # 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 # 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.7.1/passlib/handlers/digests.py0000644000175000017500000001251413015205366021560 0ustar biscuitbiscuit00000000000000"""passlib.handlers.digests - plain hash digests """ #============================================================================= # imports #============================================================================= # core import hashlib import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import to_native_str, to_bytes, render_bytes, consteq from passlib.utils.compat import unicode, str_to_uascii import passlib.utils.handlers as uh from passlib.crypto.digest import lookup_hash # 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(digest, module=__name__): # NOTE: could set digest_name=hash.name for cpython, but not for some other platforms. info = lookup_hash(digest) name = "hex_" + info.name return type(name, (HexDigestHash,), dict( name=name, __module__=module, # so ABCMeta won't clobber it _hash_func=staticmethod(info.const), # sometimes it's a function, sometimes not. so wrap it. checksum_size=info.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. """ % (info.name,) )) #============================================================================= # predefined handlers #============================================================================= hex_md4 = create_hex_hash("md4") hex_md5 = create_hex_hash("md5") hex_md5.django_name = "unsalted_md5" hex_sha1 = create_hex_hash("sha1") hex_sha256 = create_hex_hash("sha256") hex_sha512 = create_hex_hash("sha512") #============================================================================= # htdigest #============================================================================= class htdigest(uh.MinimalHandler): """htdigest hash function. .. todo:: document this hash """ name = "htdigest" setting_kwds = () context_kwds = ("user", "realm", "encoding") default_encoding = "utf-8" @classmethod def hash(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.hash(secret, user, realm, encoding) return consteq(hash, other) @classmethod def identify(cls, hash): try: cls._norm_hash(hash) except ValueError: return False return True @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls): return cls.hash("", "", "") @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config, user, realm, encoding=None): # NOTE: 'config' is ignored, as this hash has no salting / other configuration. # just have to make sure it's valid. cls._norm_hash(config) return cls.hash(secret, user, realm, encoding) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/windows.py0000644000175000017500000003014013015205366021603 0ustar biscuitbiscuit00000000000000"""passlib.handlers.nthash - Microsoft Windows -related hashes""" #============================================================================= # imports #============================================================================= # core from binascii import hexlify 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 unicode from passlib.crypto.digest import lookup_hash md4 = lookup_hash("md4").const import passlib.utils.handlers as uh # local __all__ = [ "lmhash", "nthash", "bsd_nthash", "msdcc", "msdcc2", ] #============================================================================= # lanman hash #============================================================================= class lmhash(uh.TruncateMixin, 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.using` method accepts a single optional keyword: :param bool truncate_error: By default, this will silently truncate passwords larger than 14 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 1.7 The :meth:`~passlib.ifc.PasswordHash.hash` 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 #=================================================================== #-------------------- # PasswordHash #-------------------- name = "lmhash" setting_kwds = ("truncate_error",) #-------------------- # GenericHandler #-------------------- checksum_chars = uh.HEX_CHARS checksum_size = 32 #-------------------- # TruncateMixin #-------------------- truncate_size = 14 #-------------------- # custom #-------------------- default_encoding = "cp437" #=================================================================== # methods #=================================================================== @classmethod def _norm_hash(cls, hash): return hash.lower() def _calc_checksum(self, secret): # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(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.crypto.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.hash` 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.hash` 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 hash(cls, secret, config=None): ## if config is not None and not cls.identify(config): ## raise uh.exc.InvalidHashError(cls) ## return lmhash.hash(secret) + ":" + nthash.hash(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.hash`, :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.hash`, :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.crypto.digest import pbkdf2_hmac 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_hmac("sha1", tmp, user, 10240, 16) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/postgres.py0000644000175000017500000000434213015205366021764 0ustar biscuitbiscuit00000000000000"""passlib.handlers.postgres_md5 - MD5-based algorithm used by Postgres for pg_shadow table""" #============================================================================= # imports #============================================================================= # core from hashlib import md5 import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import to_bytes from passlib.utils.compat import 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.hash`, :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.7.1/passlib/handlers/django.py0000644000175000017500000004705713015205366021372 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 logging; log = logging.getLogger(__name__) # site # pkg from passlib.handlers.bcrypt import _wrapped_bcrypt from passlib.hash import argon2, bcrypt, pbkdf2_sha1, pbkdf2_sha256 from passlib.utils import to_unicode, rng, getrandstr from passlib.utils.binary import BASE64_CHARS from passlib.utils.compat import str_to_uascii, uascii_to_str, unicode, u from passlib.crypto.digest import pbkdf2_hmac import passlib.utils.handlers as uh # local __all__ = [ "django_salted_sha1", "django_salted_md5", "django_bcrypt", "django_pbkdf2_sha1", "django_pbkdf2_sha256", "django_argon2", "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") # 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 @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) # NOTE: only used by PBKDF2 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) 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.hash` 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.hash` 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()) #============================================================================= # BCrypt #============================================================================= 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" django_bcrypt._using_clone_attrs += ("django_name",) #============================================================================= # BCRYPT + SHA256 #============================================================================= class django_bcrypt_sha256(_wrapped_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:`bcrypt ` for more details. .. versionadded:: 1.6.2 """ name = "django_bcrypt_sha256" django_name = "bcrypt_sha256" _digest = sha256 # 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 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) #============================================================================= # PBKDF2 variants #============================================================================= 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.using` method accepts 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 _digest = "sha256" def _calc_checksum(self, secret): # NOTE: secret & salt will be encoded using UTF-8 by pbkdf2_hmac() hash = pbkdf2_hmac(self._digest, secret, self.salt, self.rounds) 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.using` method accepts 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 _digest = "sha1" #============================================================================= # Argon2 #============================================================================= django_argon2 = uh.PrefixWrapper("django_argon2", argon2, prefix=u('argon2'), ident=u('argon2$argon2i$'), # 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.10's Argon2 wrapper, and follows the :ref:`password-hash-api`. This is identical to :class:`!argon2` itself, but with the Django-specific prefix ``"argon2$"`` prepended. See :doc:`argon2 ` for more details, the usage and behavior is identical. This should be compatible with the hashes generated by Django 1.10's :class:`!Argon2PasswordHasher` class. .. versionadded:: 1.7 """) django_argon2.django_name = "argon2" django_argon2._using_clone_attrs += ("django_name",) #============================================================================= # DES #============================================================================= class django_des_crypt(uh.TruncateMixin, 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.hash` 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]``. :param bool truncate_error: By default, django_des_crypt will silently truncate passwords larger than 8 bytes. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash` to raise a :exc:`~passlib.exc.PasswordTruncateError` instead. .. versionadded:: 1.7 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", "truncate_error") ident = u("crypt$") checksum_chars = salt_chars = uh.HASH64_CHARS checksum_size = 11 min_salt_size = default_salt_size = 2 truncate_size = 8 # 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 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() # check for truncation (during .hash() calls only) if self.use_defaults: self._check_truncate_policy(secret) return des_crypt(salt=self.salt[:2])._calc_checksum(secret) class django_disabled(uh.ifc.DisabledHash, 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 generated 40-char alphanumeric string to each unusuable password. This class recognizes such strings, but for backwards compatibility, still returns ``"!"``. See ``_ for why Django appends an alphanumeric string. .. versionchanged:: 1.6.2 added Django 1.6 support .. versionchanged:: 1.7 started appending an alphanumeric string. """ name = "django_disabled" _hash_prefix = u("!") suffix_length = 40 # XXX: move this to StaticHandler, or wherever _hash_prefix is being used? @classmethod def identify(cls, hash): hash = uh.to_unicode_for_identify(hash) return hash.startswith(cls._hash_prefix) def _calc_checksum(self, secret): # generate random suffix to match django's behavior return getrandstr(rng, BASE64_CHARS[:-2], self.suffix_length) @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.7.1/passlib/handlers/pbkdf2.py0000644000175000017500000004510213043774534021276 0ustar biscuitbiscuit00000000000000"""passlib.handlers.pbkdf - PBKDF2 based hashes""" #============================================================================= # imports #============================================================================= # core from binascii import hexlify, unhexlify from base64 import b64encode, b64decode import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils import to_unicode from passlib.utils.binary import ab64_decode, ab64_encode from passlib.utils.compat import str_to_bascii, u, uascii_to_str, unicode from passlib.crypto.digest import pbkdf2_hmac 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 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-- _digest = None # name of subclass-specified hash # 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): salt = ab64_encode(self.salt).decode("ascii") chk = ab64_encode(self.checksum).decode("ascii") return uh.render_mc3(self.ident, self.rounds, salt, chk) def _calc_checksum(self, secret): # NOTE: pbkdf2_hmac() will encode secret & salt using UTF8 return pbkdf2_hmac(self._digest, secret, self.salt, self.rounds, self.checksum_size) 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,) base = Pbkdf2DigestHandler return type(name, (base,), dict( __module__=module, # so ABCMeta won't clobber it. name=name, ident=ident, _digest = hash_name, default_rounds=rounds, checksum_size=digest_size, encoded_checksum_size=(digest_size*4+2)//3, __doc__="""This class implements a generic ``PBKDF2-HMAC-%(digest)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.using` method accepts 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 %(dsc)d 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(digest=hash_name.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.using` method accepts 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$") checksum_size = 20 # 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 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): salt = b64encode(self.salt, CTA_ALTCHARS).decode("ascii") chk = b64encode(self.checksum, CTA_ALTCHARS).decode("ascii") return uh.render_mc3(self.ident, self.rounds, salt, chk, rounds_base=16) #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 return pbkdf2_hmac("sha1", secret, self.salt, self.rounds, 20) #=================================================================== # 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.using` method accepts 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$") _stub_checksum = u("0" * 48 + "=") # 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 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): rounds = self.rounds if rounds == 400: rounds = None # omit rounds measurement if == 400 return uh.render_mc3(self.ident, rounds, self.salt, self.checksum, rounds_base=16) def _get_config(self): rounds = self.rounds if rounds == 400: rounds = None # omit rounds measurement if == 400 return uh.render_mc3(self.ident, rounds, self.salt, None, rounds_base=16) #=================================================================== # backend #=================================================================== def _calc_checksum(self, secret): # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 salt = self._get_config() result = pbkdf2_hmac("sha1", secret, salt, self.rounds, 24) 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.using` method accepts the following optional keywords: :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 #--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 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 # crowd seems to use a fixed number of rounds. # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 return pbkdf2_hmac("sha1", secret, self.salt, 10000, 32) #============================================================================= # 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.using` method accepts 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.") checksum_size = 64 # 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 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): salt = hexlify(self.salt).decode("ascii").upper() chk = hexlify(self.checksum).decode("ascii").upper() 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 # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8 return pbkdf2_hmac("sha512", secret, self.salt, self.rounds, 64) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/handlers/phpass.py0000644000175000017500000001126113015205366021412 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 logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils.binary import h64 from passlib.utils.compat import 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.using` method accepts 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< 16 bytes are now rejected / throw error instead of being silently truncated, to match Cisco behavior. A number of :ref:`bugs ` were fixed which caused prior releases to generate unverifiable hashes in certain cases. """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "cisco_pix" truncate_size = 16 # NOTE: these are the default policy for PasswordHash, # but want to set them explicitly for now. truncate_error = True truncate_verify_reject = True #-------------------- # GenericHandler #-------------------- checksum_size = 16 checksum_chars = uh.HASH64_CHARS #-------------------- # custom #-------------------- #: control flag signalling "cisco_asa" mode, set by cisco_asa class _is_asa = False #=================================================================== # methods #=================================================================== def _calc_checksum(self, secret): """ This function implements the "encrypted" hash format used by Cisco PIX & ASA. It's behavior has been confirmed for ASA 9.6, but is presumed correct for PIX & other ASA releases, as it fits with known test vectors, and existing literature. While nearly the same, the PIX & ASA hashes have slight differences, so this function performs differently based on the _is_asa class flag. Noteable changes from PIX to ASA include password size limit increased from 16 -> 32, and other internal changes. """ # select PIX vs or ASA mode asa = self._is_asa # # encode secret # # per ASA 8.4 documentation, # http://www.cisco.com/c/en/us/td/docs/security/asa/asa84/configuration/guide/asa_84_cli_config/ref_cli.html#Supported_Character_Sets, # it supposedly uses UTF-8 -- though some double-encoding issues have # been observed when trying to actually *set* a non-ascii password # via ASDM, and access via SSH seems to strip 8-bit chars. # if isinstance(secret, unicode): secret = secret.encode("utf-8") # # check if password too large # # Per ASA 9.6 changes listed in # http://www.cisco.com/c/en/us/td/docs/security/asa/roadmap/asa_new_features.html, # prior releases had a maximum limit of 32 characters. # Testing with an ASA 9.6 system bears this out -- # setting 32-char password for a user account, # and logins will fail if any chars are appended. # (ASA 9.6 added new PBKDF2-based hash algorithm, # which supports larger passwords). # # Per PIX documentation # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html, # it would not allow passwords > 16 chars. # # Thus, we unconditionally throw a password size error here, # as nothing valid can come from a larger password. # NOTE: assuming PIX has same behavior, but at 16 char limit. # spoil_digest = None if len(secret) > self.truncate_size: if self.use_defaults: # called from hash() msg = "Password too long (%s allows at most %d bytes)" % \ (self.name, self.truncate_size) raise uh.exc.PasswordSizeError(self.truncate_size, msg=msg) else: # called from verify() -- # We don't want to throw error, or return early, # as that would let attacker know too much. Instead, we set a # flag to add some dummy data into the md5 digest, so that # output won't match truncated version of secret, or anything # else that's fixed and predictable. spoil_digest = secret + _DUMMY_BYTES # # append user to secret # # Policy appears to be: # # * Nothing appended for enable password (user = "") # # * ASA: If user present, but secret is >= 28 chars, nothing appended. # # * 1-2 byte users not allowed. # DEVIATION: we're letting them through, and repeating their # chars ala 3-char user, to simplify testing. # Could issue warning in the future though. # # * 3 byte user has first char repeated, to pad to 4. # (observed under ASA 9.6, assuming true elsewhere) # # * 4 byte users are used directly. # # * 5+ byte users are truncated to 4 bytes. # user = self.user if user: if isinstance(user, unicode): user = user.encode("utf-8") if not asa or len(secret) < 28: secret += repeat_string(user, 4) # # pad / truncate result to limit # # While PIX always pads to 16 bytes, ASA increases to 32 bytes IFF # secret+user > 16 bytes. This makes PIX & ASA have different results # where secret size in range(13,16), and user is present -- # PIX will truncate to 16, ASA will truncate to 32. # if asa and len(secret) > 16: pad_size = 32 else: pad_size = 16 secret = right_pad_string(secret, pad_size) # # md5 digest # if spoil_digest: # make sure digest won't match truncated version of secret secret += spoil_digest digest = md5(secret).digest() # # drop every 4th byte # NOTE: guessing this was done because it makes output exactly # 16 bytes, which may have been a general 'char password[]' # size limit under PIX # digest = join_byte_elems(c for i, c in enumerate(digest) if (i + 1) & 3) # # encode using Hash64 # return h64.encode_bytes(digest).decode("ascii") # NOTE: works, but needs UTs. # @classmethod # def same_as_pix(cls, secret, user=""): # """ # test whether (secret + user) combination should # have the same hash under PIX and ASA. # # mainly present to help unittests. # """ # # see _calc_checksum() above for details of this logic. # size = len(to_bytes(secret, "utf-8")) # if user and size < 28: # size += 4 # return size < 17 #=================================================================== # eoc #=================================================================== class cisco_asa(cisco_pix): """ This class implements the password hash used by Cisco ASA/PIX 7.0 and newer (2005). Aside from a different internal algorithm, it's use and format is identical to the older :class:`cisco_pix` class. For passwords less than 13 characters, this should be identical to :class:`!cisco_pix`, but will generate a different hash for most larger inputs (See the `Format & Algorithm`_ section for the details). This class only allows passwords <= 32 bytes, anything larger will result in a :exc:`~passlib.exc.PasswordSizeError` if passed to :meth:`~cisco_asa.hash`, and be silently rejected if passed to :meth:`~cisco_asa.verify`. .. versionadded:: 1.7 .. versionchanged:: 1.7.1 Passwords > 32 bytes are now rejected / throw error instead of being silently truncated, to match Cisco behavior. A number of :ref:`bugs ` were fixed which caused prior releases to generate unverifiable hashes in certain cases. """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "cisco_asa" #-------------------- # TruncateMixin #-------------------- truncate_size = 32 #-------------------- # cisco_pix #-------------------- _is_asa = True #=================================================================== # eoc #=================================================================== #============================================================================= # type 7 #============================================================================= class cisco_type7(uh.GenericHandler): """ This class implements the "Type 7" password encoding used by Cisco IOS, and follows the :ref:`password-hash-api`. It has a simple 4-5 bit salt, but is nonetheless a reversible encoding instead of a real hash. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: int :param salt: This may be an optional salt integer drawn from ``range(0,16)``. If omitted, one will be chosen at random. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` values that are out of range. Note that while this class outputs digests in upper-case hexadecimal, it will accept lower-case as well. This class also provides the following additional method: .. automethod:: decode """ #=================================================================== # class attrs #=================================================================== #-------------------- # PasswordHash #-------------------- name = "cisco_type7" setting_kwds = ("salt",) #-------------------- # GenericHandler #-------------------- checksum_chars = uh.UPPER_HEX_CHARS #-------------------- # HasSalt #-------------------- # NOTE: encoding could handle max_salt_value=99, but since key is only 52 # chars in size, not sure what appropriate behavior is for that edge case. min_salt_value = 0 max_salt_value = 52 #=================================================================== # methods #=================================================================== @classmethod def using(cls, salt=None, **kwds): subcls = super(cisco_type7, cls).using(**kwds) if salt is not None: salt = subcls._norm_salt(salt, relaxed=kwds.get("relaxed")) subcls._generate_salt = staticmethod(lambda: salt) return subcls @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") if len(hash) < 2: raise uh.exc.InvalidHashError(cls) salt = int(hash[:2]) # may throw ValueError return cls(salt=salt, checksum=hash[2:].upper()) def __init__(self, salt=None, **kwds): super(cisco_type7, self).__init__(**kwds) if salt is not None: salt = self._norm_salt(salt) elif self.use_defaults: salt = self._generate_salt() assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,) else: raise TypeError("no salt specified") self.salt = salt @classmethod def _norm_salt(cls, salt, relaxed=False): """ validate & normalize salt value. .. note:: the salt for this algorithm is an integer 0-52, not a string """ if not isinstance(salt, int): raise uh.exc.ExpectedTypeError(salt, "integer", "salt") if 0 <= salt <= cls.max_salt_value: return salt msg = "salt/offset must be in 0..52 range" if relaxed: warn(msg, uh.PasslibHashWarning) return 0 if salt < 0 else cls.max_salt_value else: raise ValueError(msg) @staticmethod def _generate_salt(): 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.7.1/passlib/handlers/sun_md5_crypt.py0000644000175000017500000003315513015205366022715 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 to_unicode from passlib.utils.binary import h64 from passlib.utils.compat import 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" b"Whether 'tis nobler in the mind to suffer\n" b"The slings and arrows of outrageous fortune\n" b"Or to take arms against a sea of troubles,\n" b"And by opposing end them?--To die,--to sleep,--\n" b"No more; and by a sleep to say we end\n" b"The heartache, and the thousand natural shocks\n" b"That flesh is heir to,--'tis a consummation\n" b"Devoutly to be wish'd. To die,--to sleep;--\n" b"To sleep! perchance to dream:--ay, there's the rub;\n" b"For in that sleep of death what dreams may come,\n" b"When we have shuffled off this mortal coil,\n" b"Must give us pause: there's the respect\n" b"That makes calamity of so long life;\n" b"For who would bear the whips and scorns of time,\n" b"The oppressor's wrong, the proud man's contumely,\n" b"The pangs of despis'd love, the law's delay,\n" b"The insolence of office, and the spurns\n" b"That patient merit of the unworthy takes,\n" b"When he himself might his quietus make\n" b"With a bare bodkin? who would these fardels bear,\n" b"To grunt and sweat under a weary life,\n" b"But that the dread of something after death,--\n" b"The undiscover'd country, from whose bourn\n" b"No traveller returns,--puzzles the will,\n" b"And makes us rather bear those ills we have\n" b"Than fly to others that we know not of?\n" b"Thus conscience does make cowards of us all;\n" b"And thus the native hue of resolution\n" b"Is sicklied o'er with the pale cast of thought;\n" b"And enterprises of great pith and moment,\n" b"With this regard, their currents turn awry,\n" b"And lose the name of action.--Soft you now!\n" b"The fair Ophelia!--Nymph, in thy orisons\n" b"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.using` method accepts 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 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 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.7.1/passlib/handlers/misc.py0000644000175000017500000002357513016611237021061 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, str_consteq from passlib.utils.compat import unicode, u, unicode_or_bytes_types import passlib.utils.handlers as uh # local __all__ = [ "unix_disabled", "unix_fallback", "plaintext", ] #============================================================================= # handler #============================================================================= class unix_fallback(uh.ifc.DisabledHash, 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 positively identify all hash strings. * for security, passwords will always 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, unicode_or_bytes_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 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, unicode_or_bytes_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.ifc.DisabledHash, uh.MinimalHandler): """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.hash` 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 = () _disable_prefixes = tuple(str(_MARKER_CHARS)) # TODO: rename attr to 'marker'... 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 using(cls, marker=None, **kwds): subcls = super(unix_disabled, cls).using(**kwds) if marker is not None: if not cls.identify(marker): raise ValueError("invalid marker: %r" % marker) subcls.default_marker = marker return subcls @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"; # this code recognizes it, but treats it the same as "!" 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 verify(cls, secret, hash): uh.validate_secret(secret) if not cls.identify(hash): # handles typecheck raise uh.exc.InvalidHashError(cls) return False @classmethod def hash(cls, secret, **kwds): if kwds: uh.warn_hash_settings_deprecation(cls, kwds) return cls.using(**kwds).hash(secret) uh.validate_secret(secret) marker = cls.default_marker assert marker and cls.identify(marker) return to_native_str(marker, param="marker") @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config, marker=None): if not cls.identify(config): raise uh.exc.InvalidHashError(cls) elif config: # preserve the existing str,since it might contain a disabled password hash ("!" + hash) uh.validate_secret(secret) return to_native_str(config, param="config") else: if marker is not None: cls = cls.using(marker=marker) return cls.hash(secret) @classmethod def disable(cls, hash=None): out = cls.hash("") if hash is not None: hash = to_native_str(hash, param="hash") if cls.identify(hash): # extract original hash, so that we normalize marker hash = cls.enable(hash) if hash: out += hash return out @classmethod def enable(cls, hash): hash = to_native_str(hash, param="hash") for prefix in cls._disable_prefixes: if hash.startswith(prefix): orig = hash[len(prefix):] if orig: return orig else: raise ValueError("cannot restore original hash") raise uh.exc.InvalidHashError(cls) class plaintext(uh.MinimalHandler): """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. The :meth:`~passlib.ifc.PasswordHash.hash`, :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, unicode_or_bytes_types): return True else: raise uh.exc.ExpectedStringError(hash, "hash") @classmethod def hash(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 str_consteq(cls.hash(secret, encoding), hash) @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls): return cls.hash("") @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genhash(cls, secret, config, encoding=None): # NOTE: 'config' is ignored, as this hash has no salting / etc if not cls.identify(config): raise uh.exc.InvalidHashError(cls) return cls.hash(secret, encoding=encoding) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/0000755000175000017500000000000013043774617017116 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/tests/__init__.py0000644000175000017500000000002412214647077021221 0ustar biscuitbiscuit00000000000000"""passlib tests""" passlib-1.7.1/passlib/tests/test_handlers_scrypt.py0000644000175000017500000001003413043457152023721 0ustar biscuitbiscuit00000000000000"""passlib.tests.test_handlers - tests for passlib hash algorithms""" #============================================================================= # imports #============================================================================= # core import logging; log = logging.getLogger(__name__) import warnings warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*") # site # pkg from passlib import hash from passlib.tests.utils import HandlerCase, TEST_MODE from passlib.tests.test_handlers import UPASS_TABLE, PASS_TABLE_UTF8 # module #============================================================================= # scrypt hash #============================================================================= class _scrypt_test(HandlerCase): handler = hash.scrypt known_correct_hashes = [ # # excepted from test vectors from scrypt whitepaper # (http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b), # and encoded using passlib's custom format # # salt=b"" ("", "$scrypt$ln=4,r=1,p=1$$d9ZXYjhleyA7GcpCwYoEl/FrSETjB0ro39/6P+3iFEI"), # salt=b"NaCl" ("password", "$scrypt$ln=10,r=8,p=16$TmFDbA$/bq+HJ00cgB4VucZDQHp/nxq18vII3gw53N2Y0s3MWI"), # # custom # # simple test ("test", '$scrypt$ln=8,r=8,p=1$wlhLyXmP8b53bm1NKYVQqg$mTpvG8lzuuDk+DWz8HZIB6Vum6erDuUm0As5yU+VxWA'), # different block value ("password", '$scrypt$ln=8,r=2,p=1$dO6d0xoDoLT2PofQGoNQag$g/Wf2A0vhHhaJM+addK61QPBthSmYB6uVTtQzh8CM3o'), # different rounds (UPASS_TABLE, '$scrypt$ln=7,r=8,p=1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'), # alt encoding (PASS_TABLE_UTF8, '$scrypt$ln=7,r=8,p=1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'), # diff block & parallel counts as well ("nacl", '$scrypt$ln=1,r=4,p=2$yhnD+J+Tci4lZCwFgHCuVQ$fAsEWmxSHuC0cHKMwKVFPzrQukgvK09Sj+NueTSxKds') ] if TEST_MODE("full"): # add some hashes with larger rounds value. known_correct_hashes.extend([ # # from scrypt whitepaper # # salt=b"SodiumChloride" ("pleaseletmein", "$scrypt$ln=14,r=8,p=1$U29kaXVtQ2hsb3JpZGU" "$cCO9yzr9c0hGHAbNgf046/2o+7qQT44+qbVD9lRdofI"), # # openwall format (https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt) # ("pleaseletmein", "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"), ]) known_malformed_hashes = [ # missing 'p' value '$scrypt$ln=10,r=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', # rounds too low '$scrypt$ln=0,r=1,p=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', # invalid block size '$scrypt$ln=10,r=A,p=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', # r*p too large '$scrypt$ln=10,r=134217728,p=8$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ', ] def setUpWarnings(self): super(_scrypt_test, self).setUpWarnings() warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*") def populate_settings(self, kwds): # builtin is still just way too slow. if self.backend == "builtin": kwds.setdefault("rounds", 6) super(_scrypt_test, self).populate_settings(kwds) class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.randintgauss(4, 10, 6, 1) # create test cases for specific backends scrypt_scrypt_test = _scrypt_test.create_backend_case("scrypt") scrypt_builtin_test = _scrypt_test.create_backend_case("builtin") #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_utils_handlers.py0000644000175000017500000007660613016611237023552 0ustar biscuitbiscuit00000000000000"""tests for passlib.hash -- (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.exc import MissingBackendError, PasslibHashWarning from passlib.utils.compat import str_to_uascii, \ uascii_to_str, unicode import passlib.utils.handlers as uh from passlib.tests.utils import HandlerCase, TestCase from passlib.utils.compat import u # 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""" #=================================================================== # 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.assertEqual(d1.genconfig(), d1.hash("")) # 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 hash method self.assertEqual(d1.hash('s'), '_a') self.assertEqual(d1.hash('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") # NOTE: have to support hash=None since this is test of legacy 1.5 api 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() # hash should issue api warnings, but everything else should be fine. with self.assertWarningList("d1.*should be updated.*_calc_checksum"): hash = d1.hash("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.hash, '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') def norm_checksum(checksum=None, **k): return d1(checksum=checksum, **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 # NOTE: this could be turned back on if we test _norm_checksum() directly... #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.assertEqual(d1()._stub_checksum, u('xxxx')) def test_12_norm_checksum_raw(self): """test GenericHandler + HasRawChecksum mixin""" class d1(uh.HasRawChecksum, uh.GenericHandler): name = 'd1' checksum_size = 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')) # NOTE: this could be turned back on if we test _norm_checksum() directly... # self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True) # test _stub_checksum behavior self.assertEqual(d1()._stub_checksum, b'\x00'*4) 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.using(salt_size=sz, **k)(use_defaults=True).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 warnings.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) # check generated salts with warnings.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_size.*above max_salt_size"]) # 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_init_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 # NOTE: really is testing _init_rounds(), could dup to test _norm_rounds() via .replace 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 warnings.catch_warnings(record=True) as wlog: # too small self.assertRaises(ValueError, norm_rounds, rounds=0) self.consumeWarningList(wlog) # 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) # 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") _enable_a = False _enable_b = False @classmethod def _load_backend_a(cls): if cls._enable_a: cls._set_calc_checksum_backend(cls._calc_checksum_a) return True else: return False @classmethod def _load_backend_b(cls): if cls._enable_b: cls._set_calc_checksum_backend(cls._calc_checksum_b) return True else: return 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._enable_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._enable_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') # test error thrown if _has & _load are mixed d1.set_backend("b") # switch away from 'a' so next call actually checks loader class d2(d1): _has_backend_a = True self.assertRaises(AssertionError, d2.has_backend, "a") def test_41_backends(self): """test GenericHandler + HasManyBackends mixin (deprecated api)""" warnings.filterwarnings("ignore", category=DeprecationWarning, message=r".* support for \._has_backend_.* is deprecated.*", ) 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) self.assertTrue('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.assertEqual(d1.genconfig(), '{XXX}1B2M2Y8AsgTpgAmY7PhCfg==') # genhash self.assertRaises(TypeError, d1.genhash, "password", None) self.assertEqual(d1.genhash("password", dph), dph) self.assertRaises(ValueError, d1.genhash, "password", lph) # hash self.assertEqual(d1.hash("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.hash, '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:]) def to_string(self): hash = u("@salt%s%s") % (self.salt, self.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): 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): stub = SaltedHash(use_defaults=True)._stub_checksum self.assertRaises(TypeError, SaltedHash, checksum=stub, salt=None) self.assertRaises(ValueError, SaltedHash, checksum=stub, salt='xxx') #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/sample1b.cfg0000644000175000017500000000037413015205366021274 0ustar biscuitbiscuit00000000000000[passlib] schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt default = md5_crypt all__vary_rounds = 0.1 bsdi_crypt__default_rounds = 25001 bsdi_crypt__max_rounds = 30001 sha512_crypt__max_rounds = 50000 sha512_crypt__min_rounds = 40000 passlib-1.7.1/passlib/tests/test_utils_md4.py0000644000175000017500000000270213015205366022421 0ustar biscuitbiscuit00000000000000""" passlib.tests -- tests for passlib.utils.md4 .. warning:: This module & it's functions have been deprecated, and superceded by the functions in passlib.crypto. This file is being maintained until the deprecated functions are removed, and is only present prevent historical regressions up to that point. New and more thorough testing is being done by the replacement tests in ``test_utils_crypto_builtin_md4``. """ #============================================================================= # imports #============================================================================= # core import warnings # site # pkg # module from passlib.tests.test_crypto_builtin_md4 import _Common_MD4_Test # local __all__ = [ "Legacy_MD4_Test", ] #============================================================================= # test pure-python MD4 implementation #============================================================================= class Legacy_MD4_Test(_Common_MD4_Test): descriptionPrefix = "passlib.utils.md4.md4()" def setUp(self): super(Legacy_MD4_Test, self).setUp() warnings.filterwarnings("ignore", ".*passlib.utils.md4.*deprecated", DeprecationWarning) def get_md4_const(self): from passlib.utils.md4 import md4 return md4 #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_pwd.py0000644000175000017500000001602613026017171021310 0ustar biscuitbiscuit00000000000000"""passlib.tests -- tests for passlib.pwd""" #============================================================================= # imports #============================================================================= # core import itertools import logging; log = logging.getLogger(__name__) # site # pkg from passlib.tests.utils import TestCase # local __all__ = [ "UtilsTest", "GenerateTest", "StrengthTest", ] #============================================================================= # #============================================================================= class UtilsTest(TestCase): """test internal utilities""" descriptionPrefix = "passlib.pwd" def test_self_info_rate(self): """_self_info_rate()""" from passlib.pwd import _self_info_rate self.assertEqual(_self_info_rate(""), 0) self.assertEqual(_self_info_rate("a" * 8), 0) self.assertEqual(_self_info_rate("ab"), 1) self.assertEqual(_self_info_rate("ab" * 8), 1) self.assertEqual(_self_info_rate("abcd"), 2) self.assertEqual(_self_info_rate("abcd" * 8), 2) self.assertAlmostEqual(_self_info_rate("abcdaaaa"), 1.5488, places=4) # def test_total_self_info(self): # """_total_self_info()""" # from passlib.pwd import _total_self_info # # self.assertEqual(_total_self_info(""), 0) # # self.assertEqual(_total_self_info("a" * 8), 0) # # self.assertEqual(_total_self_info("ab"), 2) # self.assertEqual(_total_self_info("ab" * 8), 16) # # self.assertEqual(_total_self_info("abcd"), 8) # self.assertEqual(_total_self_info("abcd" * 8), 64) # self.assertAlmostEqual(_total_self_info("abcdaaaa"), 12.3904, places=4) #============================================================================= # word generation #============================================================================= # import subject from passlib.pwd import genword, default_charsets ascii_62 = default_charsets['ascii_62'] hex = default_charsets['hex'] class WordGeneratorTest(TestCase): """test generation routines""" descriptionPrefix = "passlib.pwd.genword()" def setUp(self): super(WordGeneratorTest, self).setUp() # patch some RNG references so they're reproducible. from passlib.pwd import SequenceGenerator self.patchAttr(SequenceGenerator, "rng", self.getRandom("pwd generator")) def assertResultContents(self, results, count, chars, unique=True): """check result list matches expected count & charset""" self.assertEqual(len(results), count) if unique: if unique is True: unique = count self.assertEqual(len(set(results)), unique) self.assertEqual(set("".join(results)), set(chars)) def test_general(self): """general behavior""" # basic usage result = genword() self.assertEqual(len(result), 9) # malformed keyword should have useful error. self.assertRaisesRegex(TypeError, "(?i)unexpected keyword.*badkwd", genword, badkwd=True) def test_returns(self): """'returns' keyword""" # returns=int option results = genword(returns=5000) self.assertResultContents(results, 5000, ascii_62) # returns=iter option gen = genword(returns=iter) results = [next(gen) for _ in range(5000)] self.assertResultContents(results, 5000, ascii_62) # invalid returns option self.assertRaises(TypeError, genword, returns='invalid-type') def test_charset(self): """'charset' & 'chars' options""" # charset option results = genword(charset="hex", returns=5000) self.assertResultContents(results, 5000, hex) # chars option # there are 3**3=27 possible combinations results = genword(length=3, chars="abc", returns=5000) self.assertResultContents(results, 5000, "abc", unique=27) # chars + charset self.assertRaises(TypeError, genword, chars='abc', charset='hex') # TODO: test rng option #============================================================================= # phrase generation #============================================================================= # import subject from passlib.pwd import genphrase simple_words = ["alpha", "beta", "gamma"] class PhraseGeneratorTest(TestCase): """test generation routines""" descriptionPrefix = "passlib.pwd.genphrase()" def assertResultContents(self, results, count, words, unique=True, sep=" "): """check result list matches expected count & charset""" self.assertEqual(len(results), count) if unique: if unique is True: unique = count self.assertEqual(len(set(results)), unique) out = set(itertools.chain.from_iterable(elem.split(sep) for elem in results)) self.assertEqual(out, set(words)) def test_general(self): """general behavior""" # basic usage result = genphrase() self.assertEqual(len(result.split(" ")), 4) # 48 / log(7776, 2) ~= 3.7 -> 4 # malformed keyword should have useful error. self.assertRaisesRegex(TypeError, "(?i)unexpected keyword.*badkwd", genphrase, badkwd=True) def test_entropy(self): """'length' & 'entropy' keywords""" # custom entropy result = genphrase(entropy=70) self.assertEqual(len(result.split(" ")), 6) # 70 / log(7776, 2) ~= 5.4 -> 6 # custom length result = genphrase(length=3) self.assertEqual(len(result.split(" ")), 3) # custom length < entropy result = genphrase(length=3, entropy=48) self.assertEqual(len(result.split(" ")), 4) # custom length > entropy result = genphrase(length=4, entropy=12) self.assertEqual(len(result.split(" ")), 4) def test_returns(self): """'returns' keyword""" # returns=int option results = genphrase(returns=1000, words=simple_words) self.assertResultContents(results, 1000, simple_words) # returns=iter option gen = genphrase(returns=iter, words=simple_words) results = [next(gen) for _ in range(1000)] self.assertResultContents(results, 1000, simple_words) # invalid returns option self.assertRaises(TypeError, genphrase, returns='invalid-type') def test_wordset(self): """'wordset' & 'words' options""" # wordset option results = genphrase(words=simple_words, returns=5000) self.assertResultContents(results, 5000, simple_words) # words option results = genphrase(length=3, words=simple_words, returns=5000) self.assertResultContents(results, 5000, simple_words, unique=3**3) # words + wordset self.assertRaises(TypeError, genphrase, words=simple_words, wordset='bip39') #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_crypto_digest.py0000644000175000017500000004435613015205366023407 0ustar biscuitbiscuit00000000000000"""tests for passlib.utils.(des|pbkdf2|md4)""" #============================================================================= # imports #============================================================================= from __future__ import with_statement, division # core from binascii import hexlify import hashlib import warnings # site # pkg # module from passlib.utils.compat import PY3, u, JYTHON from passlib.tests.utils import TestCase, TEST_MODE, skipUnless, hb #============================================================================= # test assorted crypto helpers #============================================================================= class HashInfoTest(TestCase): """test various crypto functions""" descriptionPrefix = "passlib.crypto.digest" #: list of formats norm_hash_name() should support norm_hash_formats = ["hashlib", "iana"] #: test cases for norm_hash_name() #: each row contains (iana name, hashlib name, ... 0+ unnormalized names) norm_hash_samples = [ # real hashes ("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"), # fake hashes (to check if fallback normalization behaves sanely) ("sha4_256", "sha4-256", "SHA4-256", "SHA-4-256"), ("test128", "test-128", "TEST128"), ("test2", "test2", "TEST-2"), ("test3_128", "test3-128", "TEST-3-128"), ] def test_norm_hash_name(self): """norm_hash_name()""" from itertools import chain from passlib.crypto.digest import norm_hash_name, _known_hash_names # snapshot warning state, ignore unknown hash warnings ctx = warnings.catch_warnings() ctx.__enter__() self.addCleanup(ctx.__exit__) warnings.filterwarnings("ignore", '.*unknown hash') # test string 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 for row in chain(_known_hash_names, self.norm_hash_samples): for idx, format in enumerate(self.norm_hash_formats): correct = row[idx] for value in row: result = norm_hash_name(value, format) self.assertEqual(result, correct, "name=%r, format=%r:" % (value, format)) def test_lookup_hash_ctor(self): """lookup_hash() -- constructor""" from passlib.crypto.digest import lookup_hash # invalid/unknown names should be rejected self.assertRaises(ValueError, lookup_hash, "new") self.assertRaises(ValueError, lookup_hash, "__name__") self.assertRaises(ValueError, lookup_hash, "sha4") # 1. should return hashlib builtin if found self.assertEqual(lookup_hash("md5"), (hashlib.md5, 16, 64)) # 2. should return wrapper around hashlib.new() if found try: hashlib.new("sha") has_sha = True except ValueError: has_sha = False if has_sha: record = lookup_hash("sha") const = record[0] self.assertEqual(record, (const, 20, 64)) self.assertEqual(hexlify(const(b"abc").digest()), b"0164b8a914cd2a5e74c4f7ff082c4d97f1edf880") else: self.assertRaises(ValueError, lookup_hash, "sha") # 3. should fall back to builtin md4 try: hashlib.new("md4") has_md4 = True except ValueError: has_md4 = False record = lookup_hash("md4") const = record[0] if not has_md4: from passlib.crypto._md4 import md4 self.assertIs(const, md4) self.assertEqual(record, (const, 16, 64)) self.assertEqual(hexlify(const(b"abc").digest()), b"a448017aaf21d8525fc10ae87aa6729d") # 4. unknown names should be rejected self.assertRaises(ValueError, lookup_hash, "xxx256") # should memoize records self.assertIs(lookup_hash("md5"), lookup_hash("md5")) def test_lookup_hash_metadata(self): """lookup_hash() -- metadata""" from passlib.crypto.digest import lookup_hash # quick test of metadata using known reference - sha256 info = lookup_hash("sha256") self.assertEqual(info.name, "sha256") self.assertEqual(info.iana_name, "sha-256") self.assertEqual(info.block_size, 64) self.assertEqual(info.digest_size, 32) self.assertIs(lookup_hash("SHA2-256"), info) # quick test of metadata using known reference - md5 info = lookup_hash("md5") self.assertEqual(info.name, "md5") self.assertEqual(info.iana_name, "md5") self.assertEqual(info.block_size, 64) self.assertEqual(info.digest_size, 16) def test_lookup_hash_alt_types(self): """lookup_hash() -- alternate types""" from passlib.crypto.digest import lookup_hash info = lookup_hash("sha256") self.assertIs(lookup_hash(info), info) self.assertIs(lookup_hash(info.const), info) self.assertRaises(TypeError, lookup_hash, 123) # TODO: write full test of compile_hmac() -- currently relying on pbkdf2_hmac() tests #============================================================================= # test PBKDF1 support #============================================================================= class Pbkdf1_Test(TestCase): """test kdf helpers""" descriptionPrefix = "passlib.crypto.digest.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 JYTHON: # FIXME: find out why not jython, or reenable this. pbkdf1_tests.append( (b'password', b'salt', 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453')) ) def test_known(self): """test reference vectors""" from passlib.crypto.digest import pbkdf1 for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests: result = pbkdf1(digest, secret, salt, rounds, keylen) self.assertEqual(result, correct) def test_border(self): """test border cases""" from passlib.crypto.digest import pbkdf1 def helper(secret=b'secret', salt=b'salt', rounds=1, keylen=1, hash='md5'): return pbkdf1(hash, secret, salt, rounds, keylen) 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-HMAC support #============================================================================= # import the test subject from passlib.crypto.digest import pbkdf2_hmac, PBKDF2_BACKENDS # NOTE: relying on tox to verify this works under all the various backends. class Pbkdf2Test(TestCase): """test pbkdf2() support""" descriptionPrefix = "passlib.crypto.digest.pbkdf2_hmac() " % ", ".join(PBKDF2_BACKENDS) pbkdf2_test_vectors = [ # (result, secret, salt, rounds, keylen, digest="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 ##( ## ## hb("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, "sha512" ), # # test vectors from fastpbkdf2 # ( hb('55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc' '49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783'), b'passwd', b'salt', 1, 64, 'sha256', ), ( hb('4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56' 'a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d'), b'Password', b'NaCl', 80000, 64, 'sha256', ), ( hb('120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b'), b'password', b'salt', 1, 32, 'sha256', ), ( hb('ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43'), b'password', b'salt', 2, 32, 'sha256', ), ( hb('c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a'), b'password', b'salt', 4096, 32, 'sha256', ), ( hb('348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c' '635518c7dac47e9'), b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 40, 'sha256', ), ( hb('9e83f279c040f2a11aa4a02b24c418f2d3cb39560c9627fa4f47e3bcc2897c3d'), b'', b'salt', 1024, 32, 'sha256', ), ( hb('ea5808411eb0c7e830deab55096cee582761e22a9bc034e3ece925225b07bf46'), b'password', b'', 1024, 32, 'sha256', ), ( hb('89b69d0516f829893c696226650a8687'), b'pass\x00word', b'sa\x00lt', 4096, 16, 'sha256', ), ( hb('867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252'), b'password', b'salt', 1, 32, 'sha512', ), ( hb('e1d9c16aa681708a45f5c7c4e215ceb66e011a2e9f0040713f18aefdb866d53c'), b'password', b'salt', 2, 32, 'sha512', ), ( hb('d197b1b33db0143e018b12f3d1d1479e6cdebdcc97c5c0f87f6902e072f457b5'), b'password', b'salt', 4096, 32, 'sha512', ), ( hb('6e23f27638084b0f7ea1734e0d9841f55dd29ea60a834466f3396bac801fac1eeb' '63802f03a0b4acd7603e3699c8b74437be83ff01ad7f55dac1ef60f4d56480c35e' 'e68fd52c6936'), b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt', 1, 72, 'sha512', ), ( hb('0c60c80f961f0e71f3a9b524af6012062fe037a6'), b'password', b'salt', 1, 20, 'sha1', ), # # custom tests # ( hb('e248fb6b13365146f8ac6307cc222812'), b"secret", b"salt", 10, 16, "sha1", ), ( hb('e248fb6b13365146f8ac6307cc2228127872da6d'), b"secret", b"salt", 10, None, "sha1", ), ( hb('b1d5485772e6f76d5ebdc11b38d3eff0a5b2bd50dc11f937e86ecacd0cd40d1b' '9113e0734e3b76a3'), b"secret", b"salt", 62, 40, "md5", ), ( hb('ea014cc01f78d3883cac364bb5d054e2be238fb0b6081795a9d84512126e3129' '062104d2183464c4'), b"secret", b"salt", 62, 40, "md4", ), ] def test_known(self): """test reference vectors""" for row in self.pbkdf2_test_vectors: correct, secret, salt, rounds, keylen = row[:5] digest = row[5] if len(row) == 6 else "sha1" result = pbkdf2_hmac(digest, secret, salt, rounds, keylen) self.assertEqual(result, correct) def test_backends(self): """verify expected backends are present""" from passlib.crypto.digest import PBKDF2_BACKENDS # check for fastpbkdf2 try: import fastpbkdf2 has_fastpbkdf2 = True except ImportError: has_fastpbkdf2 = False self.assertEqual("fastpbkdf2" in PBKDF2_BACKENDS, has_fastpbkdf2) # check for hashlib try: from hashlib import pbkdf2_hmac has_hashlib_ssl = pbkdf2_hmac.__module__ != "hashlib" except ImportError: has_hashlib_ssl = False self.assertEqual("hashlib-ssl" in PBKDF2_BACKENDS, has_hashlib_ssl) # check for appropriate builtin from passlib.utils.compat import PY3 if PY3: self.assertIn("builtin-from-bytes", PBKDF2_BACKENDS) else: # XXX: only true as long as this is preferred over hexlify self.assertIn("builtin-unpack", PBKDF2_BACKENDS) def test_border(self): """test border cases""" def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"): return pbkdf2_hmac(digest, secret, salt, rounds, keylen) helper() # invalid rounds self.assertRaises(ValueError, helper, rounds=-1) self.assertRaises(ValueError, helper, rounds=0) self.assertRaises(TypeError, helper, rounds='x') # invalid keylen helper(keylen=1) self.assertRaises(ValueError, helper, keylen=-1) self.assertRaises(ValueError, helper, keylen=0) # NOTE: hashlib actually throws error for keylen>=MAX_SINT32, # but pbkdf2 forbids anything > MAX_UINT32 * digest_size self.assertRaises(OverflowError, 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, digest='foo') self.assertRaises(TypeError, helper, digest=5) def test_default_keylen(self): """test keylen==None""" def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"): return pbkdf2_hmac(digest, secret, salt, rounds, keylen) self.assertEqual(len(helper(digest='sha1')), 20) self.assertEqual(len(helper(digest='sha256')), 32) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_apache.py0000644000175000017500000006117013015205366021742 0ustar biscuitbiscuit00000000000000"""tests for passlib.apache -- (c) Assurance Technologies 2008-2011""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from logging import getLogger import os # site # pkg from passlib import apache from passlib.exc import MissingBackendError from passlib.utils.compat import irange from passlib.tests.utils import TestCase, get_file, set_file, ensure_mtime_changed from passlib.utils.compat import u from passlib.utils import to_bytes # 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' b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n' b'user4:pass4\n' b'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' b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n' b'user4:pass4\n' b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n' b'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' b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n' b'user4:pass4\n' b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n' b'user5:$2a$12$yktDxraxijBZ360orOyCOePFGhuis/umyPNJoL5EbsLk.s6SWdrRO\n' b'user6:$5$rounds=110000$cCRp/xUUGVgwR4aP$' b'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") # alias resolution self.assertEqual(check("portable"), apache.htpasswd_defaults["portable"]) self.assertEqual(check("portable_apache_22"), apache.htpasswd_defaults["portable_apache_22"]) self.assertEqual(check("host_apache_22"), apache.htpasswd_defaults["host_apache_22"]) # default self.assertEqual(check(None), apache.htpasswd_defaults["portable_apache_22"]) 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(sorted(ht.users()), ["user1", "user2", "user3", "user4", "user5"]) 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) def test_13_whitespace(self): """whitespace & comment handling""" # per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c), # lines that match "^\s*(#.*)?$" should be ignored source = to_bytes( '\n' 'user2:pass2\n' 'user4:pass4\n' 'user7:pass7\r\n' ' \t \n' 'user1:pass1\n' ' # legacy users\n' '#user6:pass6\n' 'user5:pass5\n\n' ) # loading should see all users (except user6, who was commented out) ht = apache.HtpasswdFile.from_string(source) self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user7"]) # update existing user ht.set_hash("user4", "althash4") self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user7"]) # add a new user ht.set_hash("user6", "althash6") self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user6", "user7"]) # delete existing user ht.delete("user7") self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user6"]) # re-serialization should preserve whitespace target = to_bytes( '\n' 'user2:pass2\n' 'user4:althash4\n' ' \t \n' 'user1:pass1\n' ' # legacy users\n' '#user6:pass6\n' 'user5:pass5\n' 'user6:althash6\n' ) self.assertEqual(ht.to_string(), target) #=================================================================== # eoc #=================================================================== #============================================================================= # htdigest #============================================================================= class HtdigestFileTest(TestCase): """test HtdigestFile class""" descriptionPrefix = "HtdigestFile" # sample with 4 users sample_01 = (b'user2:realm:549d2a5f4659ab39a80dac99e159ab19\n' b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n' b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n' b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n') # sample 1 with user 1, 2 deleted; 4 changed sample_02 = (b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n' b'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' b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n' b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n' b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n' b'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(sorted(ht.users("realm")), ["user1", "user2", "user3", "user4", "user5"]) 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.7.1/passlib/tests/test_utils.py0000644000175000017500000011646213015205366021666 0ustar biscuitbiscuit00000000000000"""tests for passlib.util""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from functools import partial import warnings # site # pkg # module from passlib.utils import is_ascii_safe from passlib.utils.compat import irange, PY2, PY3, u, unicode, join_bytes, PYPY from passlib.tests.utils import TestCase, hb, run_with_fixed_seeds #============================================================================= # 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) self.assertTrue('UnicodeIO' in dir(compat)) self.assertTrue('irange' in dir(compat)) def test_classproperty(self): from passlib.utils.decor 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.decor 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.decor 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 if not PY3: self.assertIs(prop.im_func, prop.__func__) def test_getrandbytes(self): """getrandbytes()""" from passlib.utils import getrandbytes wrapper = partial(getrandbytes, self.getRandom()) self.assertEqual(len(wrapper(0)), 0) a = wrapper(10) b = wrapper(10) self.assertIsInstance(a, bytes) self.assertEqual(len(a), 10) self.assertEqual(len(b), 10) self.assertNotEqual(a, b) @run_with_fixed_seeds(count=1024) def test_getrandstr(self, seed): """getrandstr()""" from passlib.utils import getrandstr wrapper = partial(getrandstr, self.getRandom(seed=seed)) # count 0 self.assertEqual(wrapper('abc',0), '') # count <0 self.assertRaises(ValueError, wrapper, 'abc', -1) # letters 0 self.assertRaises(ValueError, wrapper, '', 0) # letters 1 self.assertEqual(wrapper('a', 5), 'aaaaa') # NOTE: the following parts are non-deterministic, # with a small chance of failure (outside chance it may pick # a string w/o one char, even more remote chance of picking # same string). to combat this, we run it against multiple # fixed seeds (using run_with_fixed_seeds decorator), # and hope that they're sufficient to test the range of behavior. # letters x = wrapper(u('abc'), 32) y = wrapper(u('abc'), 32) self.assertIsInstance(x, unicode) self.assertNotEqual(x,y) self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')]) # bytes x = wrapper(b'abc', 32) y = wrapper(b'abc', 32) 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')]) def test_generate_password(self): """generate_password()""" from passlib.utils import generate_password warnings.filterwarnings("ignore", "The function.*generate_password\(\) is deprecated") 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, 10**10) rng = random.Random(genseed()) b = rng.randint(0, 10**10) 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, str_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'') def consteq_supports_string(value): # under PY2, it supports all unicode strings (when present at all), # under PY3, compare_digest() only supports ascii unicode strings. # confirmed for: cpython 2.7.9, cpython 3.4, pypy, pypy3, pyston return (consteq is str_consteq or PY2 or is_ascii_safe(value)) # check equal inputs compare correctly for value in [ u("a"), u("abc"), u("\xff\xa2\x12\x00")*10, ]: if consteq_supports_string(value): self.assertTrue(consteq(value, value), "value %r:" % (value,)) else: self.assertRaises(TypeError, consteq, value, value) self.assertTrue(str_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")), ]: if consteq_supports_string(l) and consteq_supports_string(r): self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) else: self.assertRaises(TypeError, consteq, l, r) self.assertRaises(TypeError, consteq, r, l) self.assertFalse(str_consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(str_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 = rng.randint(1 if saw_zero else 0, 12) if not size: saw_zero = True enc_size = (4*size+2)//3 raw = getrandbytes(rng, 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 += rng.choice([-1,1,2]) raw_size = 3*size//4 encoded = getrandstr(rng, 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 getrandstr rng = self.getRandom() 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""" rng = self.getRandom() 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< 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 hash / 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') # unicode salts accepted as of passlib 1.7 (previous caused TypeError) self.assertEqual(hash(u("IX"), s1.decode("latin-1"), 1000, 'sha1'), d1) def test_94_saslprep(self): """test hash/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. # hash unnormalized h = self.do_encrypt(u("I\u00ADX")) self.assertTrue(self.do_verify(u("IX"), h)) self.assertTrue(self.do_verify(u("\u2168"), h)) # hash 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_94_using_w_default_algs(self, param="default_algs"): """using() -- 'default_algs' parameter""" # create subclass handler = self.handler orig = list(handler.default_algs) # in case it's modified in place subcls = handler.using(**{param: "sha1,md5"}) # shouldn't have changed handler self.assertEqual(handler.default_algs, orig) # should have own set self.assertEqual(subcls.default_algs, ["md5", "sha-1"]) # test hash output h1 = subcls.hash("dummy") self.assertEqual(handler.extract_digest_algs(h1), ["md5", "sha-1"]) def test_94_using_w_algs(self): """using() -- 'algs' parameter""" self.test_94_using_w_default_algs(param="algs") def test_94_needs_update_algs(self): """needs_update() -- algs setting""" handler1 = self.handler.using(algs="sha1,md5") # shouldn't need update, has same algs h1 = handler1.hash("dummy") self.assertFalse(handler1.needs_update(h1)) # *currently* shouldn't need update, has superset of algs required by handler2 # (may change this policy) handler2 = handler1.using(algs="sha1") self.assertFalse(handler2.needs_update(h1)) # should need update, doesn't have all algs required by handler3 handler3 = handler1.using(algs="sha1,sha256") self.assertTrue(handler3.needs_update(h1)) 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.hash("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) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_apps.py0000644000175000017500000001224113015205366021457 0ustar biscuitbiscuit00000000000000"""test passlib.apps""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib import apps, hash as hashmod from passlib.tests.utils import TestCase # module #============================================================================= # test predefined app contexts #============================================================================= class AppsTest(TestCase): """perform general tests to make sure contexts work""" # NOTE: these tests are not really comprehensive, # since they would do little but duplicate # the presets in apps.py # # they mainly try to ensure no typos # or dynamic behavior foul-ups. def test_master_context(self): ctx = apps.master_context self.assertGreater(len(ctx.schemes()), 50) def test_custom_app_context(self): ctx = apps.custom_app_context self.assertEqual(ctx.schemes(), ("sha512_crypt", "sha256_crypt")) for hash in [ ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6' 'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'), ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny' 'xDGgMlDcOsfaI17'), ]: self.assertTrue(ctx.verify("test", hash)) def test_django16_context(self): ctx = apps.django16_context for hash in [ 'pbkdf2_sha256$29000$ZsgquwnCyBs2$fBxRQpfKd2PIeMxtkKPy0h7SrnrN+EU/cm67aitoZ2s=', 'sha1$0d082$cdb462ae8b6be8784ef24b20778c4d0c82d5957f', 'md5$b887a$37767f8a745af10612ad44c80ff52e92', 'crypt$95a6d$95x74hLDQKXI2', '098f6bcd4621d373cade4e832627b4f6', ]: self.assertTrue(ctx.verify("test", hash)) self.assertEqual(ctx.identify("!"), "django_disabled") self.assertFalse(ctx.verify("test", "!")) def test_django_context(self): ctx = apps.django_context for hash in [ 'pbkdf2_sha256$29000$ZsgquwnCyBs2$fBxRQpfKd2PIeMxtkKPy0h7SrnrN+EU/cm67aitoZ2s=', ]: self.assertTrue(ctx.verify("test", hash)) self.assertEqual(ctx.identify("!"), "django_disabled") self.assertFalse(ctx.verify("test", "!")) def test_ldap_nocrypt_context(self): ctx = apps.ldap_nocrypt_context for hash in [ '{SSHA}cPusOzd6d5n3OjSVK3R329ZGCNyFcC7F', 'test', ]: self.assertTrue(ctx.verify("test", hash)) self.assertIs(ctx.identify('{CRYPT}$5$rounds=31817$iZGmlyBQ99JSB5' 'n6$p4E.pdPBWx19OajgjLRiOW0itGnyxDGgMlDcOsfaI17'), None) def test_ldap_context(self): ctx = apps.ldap_context for hash in [ ('{CRYPT}$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0' 'itGnyxDGgMlDcOsfaI17'), '{SSHA}cPusOzd6d5n3OjSVK3R329ZGCNyFcC7F', 'test', ]: self.assertTrue(ctx.verify("test", hash)) def test_ldap_mysql_context(self): ctx = apps.mysql_context for hash in [ '*94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29', '378b243e220ca493', ]: self.assertTrue(ctx.verify("test", hash)) def test_postgres_context(self): ctx = apps.postgres_context hash = 'md55d9c68c6c50ed3d02a2fcf54f63993b6' self.assertTrue(ctx.verify("test", hash, user='user')) def test_phppass_context(self): ctx = apps.phpass_context for hash in [ '$P$8Ja1vJsKa5qyy/b3mCJGXM7GyBnt6..', '$H$8b95CoYQnQ9Y6fSTsACyphNh5yoM02.', '_cD..aBxeRhYFJvtUvsI', ]: self.assertTrue(ctx.verify("test", hash)) h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" if hashmod.bcrypt.has_backend(): self.assertTrue(ctx.verify("test", h1)) self.assertEqual(ctx.default_scheme(), "bcrypt") self.assertEqual(ctx.handler().name, "bcrypt") else: self.assertEqual(ctx.identify(h1), "bcrypt") self.assertEqual(ctx.default_scheme(), "phpass") self.assertEqual(ctx.handler().name, "phpass") def test_phpbb3_context(self): ctx = apps.phpbb3_context for hash in [ '$P$8Ja1vJsKa5qyy/b3mCJGXM7GyBnt6..', '$H$8b95CoYQnQ9Y6fSTsACyphNh5yoM02.', ]: self.assertTrue(ctx.verify("test", hash)) self.assertTrue(ctx.hash("test").startswith("$H$")) def test_roundup_context(self): ctx = apps.roundup_context for hash in [ '{PBKDF2}9849$JMTYu3eOUSoFYExprVVqbQ$N5.gV.uR1.BTgLSvi0qyPiRlGZ0', '{SHA}a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', '{CRYPT}dptOmKDriOGfU', '{plaintext}test', ]: self.assertTrue(ctx.verify("test", hash)) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_utils_pbkdf2.py0000644000175000017500000002751413015205366023115 0ustar biscuitbiscuit00000000000000""" passlib.tests -- tests for passlib.utils.pbkdf2 .. warning:: This module & it's functions have been deprecated, and superceded by the functions in passlib.crypto. This file is being maintained until the deprecated functions are removed, and is only present prevent historical regressions up to that point. New and more thorough testing is being done by the replacement tests in ``test_utils_crypto.py``. """ #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import hashlib import warnings # site # pkg # module from passlib.utils.compat import u, JYTHON from passlib.tests.utils import TestCase, hb #============================================================================= # test assorted crypto helpers #============================================================================= class UtilsTest(TestCase): """test various utils functions""" descriptionPrefix = "passlib.utils.pbkdf2" 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"), ("test3_128", "test3-128", "TEST-3-128"), ] def setUp(self): super(UtilsTest, self).setUp() warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning) def test_norm_hash_name(self): """norm_hash_name()""" from itertools import chain from passlib.utils.pbkdf2 import norm_hash_name from passlib.crypto.digest import _known_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 warnings.catch_warnings(): warnings.filterwarnings("ignore", '.*unknown hash') for row in chain(_known_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)) #============================================================================= # test PBKDF1 support #============================================================================= class Pbkdf1_Test(TestCase): """test kdf helpers""" descriptionPrefix = "passlib.utils.pbkdf2.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 JYTHON: pbkdf1_tests.append( (b'password', b'salt', 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453')) ) def setUp(self): super(Pbkdf1_Test, self).setUp() warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning) 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""" descriptionPrefix = "passlib.utils.pbkdf2.pbkdf2()" 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 setUp(self): super(Pbkdf2_Test, self).setUp() warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning) 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=-1) self.assertRaises(ValueError, helper, rounds=0) self.assertRaises(TypeError, helper, rounds='x') # invalid keylen self.assertRaises(ValueError, helper, keylen=-1) self.assertRaises(ValueError, helper, keylen=0) helper(keylen=1) self.assertRaises(OverflowError, 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(NotImplementedError, 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() self.assertRaises(NotImplementedError, pbkdf2, b'secret', b'salt', 1000, 20, prf) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_ext_django_source.py0000644000175000017500000002543213043457152024227 0ustar biscuitbiscuit00000000000000""" test passlib.ext.django against django source tests """ #============================================================================= # imports #============================================================================= from __future__ import absolute_import, division, print_function # core import logging; log = logging.getLogger(__name__) # site # pkg from passlib.utils.compat import suppress_cause from passlib.ext.django.utils import DJANGO_VERSION, DjangoTranslator, _PasslibHasherWrapper # tests from passlib.tests.utils import TestCase, TEST_MODE from .test_ext_django import ( has_min_django, stock_config, _ExtensionSupport, ) if has_min_django: from .test_ext_django import settings # local __all__ = [ "HashersTest", ] #============================================================================= # HashersTest -- # 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 #============================================================================= #: ref to django unittest root module (if found) test_hashers_mod = None #: message about why test module isn't present (if not found) hashers_skip_msg = None #---------------------------------------------------------------------- # try to load django's tests/auth_tests/test_hasher.py module, # or note why we failed. #---------------------------------------------------------------------- if TEST_MODE(max="quick"): hashers_skip_msg = "requires >= 'default' test mode" elif has_min_django: 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) try: from auth_tests import test_hashers as test_hashers_mod except ImportError as err: raise suppress_cause( EnvironmentError("error trying to import django tests " "from source path (%r): %r" % (source_path, err))) finally: sys.path.remove(tests_path) else: hashers_skip_msg = "requires PASSLIB_TESTS_DJANGO_SOURCE_PATH to be set" if TEST_MODE("full"): # print warning so user knows what's happening sys.stderr.write("\nWARNING: $PASSLIB_TESTS_DJANGO_SOURCE_PATH is not set; " "can't run Django's own unittests against passlib.ext.django\n") elif DJANGO_VERSION: hashers_skip_msg = "django version too old" else: hashers_skip_msg = "django not installed" #---------------------------------------------------------------------- # if found module, create wrapper to run django's own tests, # but with passlib monkeypatched in. #---------------------------------------------------------------------- if test_hashers_mod: from django.core.signals import setting_changed from django.dispatch import receiver from django.utils.module_loading import import_string from passlib.utils.compat import get_unbound_method_function class HashersTest(test_hashers_mod.TestUtilsHashPass, _ExtensionSupport): """ Run django's hasher unittests against passlib's extension and workalike implementations """ #================================================================== # helpers #================================================================== # port patchAttr() helper method from passlib.tests.utils.TestCase patchAttr = get_unbound_method_function(TestCase.patchAttr) #================================================================== # custom setup #================================================================== def setUp(self): #--------------------------------------------------------- # install passlib.ext.django adapter, and get context #--------------------------------------------------------- self.load_extension(PASSLIB_CONTEXT=stock_config, check=False) from passlib.ext.django.models import adapter context = adapter.context #--------------------------------------------------------- # patch tests module to use our versions of patched funcs # (which should be installed in hashers module) #--------------------------------------------------------- from django.contrib.auth import hashers for attr in ["make_password", "check_password", "identify_hasher", "is_password_usable", "get_hasher"]: self.patchAttr(test_hashers_mod, attr, getattr(hashers, attr)) #--------------------------------------------------------- # django tests expect empty django_des_crypt salt field #--------------------------------------------------------- from passlib.hash import django_des_crypt self.patchAttr(django_des_crypt, "use_duplicate_salt", False) #--------------------------------------------------------- # install receiver to update scheme list if test changes settings #--------------------------------------------------------- django_to_passlib_name = DjangoTranslator().django_to_passlib_name @receiver(setting_changed, weak=False) def update_schemes(**kwds): if kwds and kwds['setting'] != 'PASSWORD_HASHERS': return assert context is adapter.context schemes = [ django_to_passlib_name(import_string(hash_path)()) for hash_path in settings.PASSWORD_HASHERS ] # workaround for a few tests that only specify hex_md5, # but test for django_salted_md5 format. if "hex_md5" in schemes and "django_salted_md5" not in schemes: schemes.append("django_salted_md5") schemes.append("django_disabled") context.update(schemes=schemes, deprecated="auto") adapter.reset_hashers() self.addCleanup(setting_changed.disconnect, update_schemes) update_schemes() #--------------------------------------------------------- # need password_context to keep up to date with django_hasher.iterations, # which is frequently patched by django tests. # # HACK: to fix this, inserting wrapper around a bunch of context # methods so that any time adapter calls them, # attrs are resynced first. #--------------------------------------------------------- def update_rounds(): """ sync django hasher config -> passlib hashers """ for handler in context.schemes(resolve=True): if 'rounds' not in handler.setting_kwds: continue hasher = adapter.passlib_to_django(handler) if isinstance(hasher, _PasslibHasherWrapper): continue rounds = getattr(hasher, "rounds", None) or \ getattr(hasher, "iterations", None) if rounds is None: continue # XXX: this doesn't modify the context, which would # cause other weirdness (since it would replace handler factories completely, # instead of just updating their state) handler.min_desired_rounds = handler.max_desired_rounds = handler.default_rounds = rounds _in_update = [False] def update_wrapper(wrapped, *args, **kwds): """ wrapper around arbitrary func, that first triggers sync """ if not _in_update[0]: _in_update[0] = True try: update_rounds() finally: _in_update[0] = False return wrapped(*args, **kwds) # sync before any context call for attr in ["schemes", "handler", "default_scheme", "hash", "verify", "needs_update", "verify_and_update"]: self.patchAttr(context, attr, update_wrapper, wrap=True) # sync whenever adapter tries to resolve passlib hasher self.patchAttr(adapter, "django_to_passlib", update_wrapper, wrap=True) def tearDown(self): # NOTE: could rely on addCleanup() instead, but need py26 compat self.unload_extension() super(HashersTest, self).tearDown() #================================================================== # skip a few methods that can't be replicated properly # *want to minimize these as much as possible* #================================================================== _OMIT = lambda self: self.skipTest("omitted by passlib") # XXX: this test registers two classes w/ same algorithm id, # something we don't support -- how does django sanely handle # that anyways? get_hashers_by_algorithm() should throw KeyError, right? test_pbkdf2_upgrade_new_hasher = _OMIT # TODO: support wrapping django's harden-runtime feature? # would help pass their tests. test_check_password_calls_harden_runtime = _OMIT test_bcrypt_harden_runtime = _OMIT test_pbkdf2_harden_runtime = _OMIT #================================================================== # eoc #================================================================== else: # otherwise leave a stub so test log tells why test was skipped. class HashersTest(TestCase): def test_external_django_hasher_tests(self): """external django hasher tests""" raise self.skipTest(hashers_skip_msg) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_totp.py0000644000175000017500000020003213043701620021472 0ustar biscuitbiscuit00000000000000"""passlib.tests -- test passlib.totp""" #============================================================================= # imports #============================================================================= # core import datetime from functools import partial import logging; log = logging.getLogger(__name__) import sys import time as _time # site # pkg from passlib import exc from passlib.utils.compat import unicode, u from passlib.tests.utils import TestCase, time_call # subject from passlib import totp as totp_module from passlib.totp import TOTP, AppWallet, AES_SUPPORT # local __all__ = [ "EngineTest", ] #============================================================================= # helpers #============================================================================= # XXX: python 3 changed what error base64.b16decode() throws, from TypeError to base64.Error(). # it wasn't until 3.3 that base32decode() also got changed. # really should normalize this in the code to a single BinaryDecodeError, # predicting this cross-version is getting unmanagable. Base32DecodeError = Base16DecodeError = TypeError if sys.version_info >= (3,0): from binascii import Error as Base16DecodeError if sys.version_info >= (3,3): from binascii import Error as Base32DecodeError PASS1 = "abcdef" PASS2 = b"\x00\xFF" KEY1 = '4AOGGDBBQSYHNTUZ' KEY1_RAW = b'\xe0\x1cc\x0c!\x84\xb0v\xce\x99' KEY2_RAW = b'\xee]\xcb9\x870\x06 D\xc8y/\xa54&\xe4\x9c\x13\xc2\x18' KEY3 = 'S3JDVB7QD2R7JPXX' # used in docstrings KEY4 = 'JBSWY3DPEHPK3PXP' # from google keyuri spec KEY4_RAW = b'Hello!\xde\xad\xbe\xef' # NOTE: for randtime() below, # * want at least 7 bits on fractional side, to test fractional times to at least 0.01s precision # * want at least 32 bits on integer side, to test for 32-bit epoch issues. # most systems *should* have 53 bit mantissa, leaving plenty of room on both ends, # so using (1<<37) as scale, to allocate 16 bits on fractional side, but generate reasonable # of > 1<<32 times. # sanity check that we're above 44 ensures minimum requirements (44 - 37 int = 7 frac) assert sys.float_info.radix == 2, "unexpected float_info.radix" assert sys.float_info.mant_dig >= 44, "double precision unexpectedly small" def _get_max_time_t(): """ helper to calc max_time_t constant (see below) """ value = 1 << 30 # even for 32 bit systems will handle this year = 0 while True: next_value = value << 1 try: next_year = datetime.datetime.utcfromtimestamp(next_value-1).year except (ValueError, OSError, OverflowError): # utcfromtimestamp() may throw any of the following: # # * year out of range for datetime: # py < 3.6 throws ValueError. # (py 3.6.0 returns odd value instead, see workaround below) # # * int out of range for host's gmtime/localtime: # py2 throws ValueError, py3 throws OSError. # # * int out of range for host's time_t: # py2 throws ValueError, py3 throws OverflowError. # break # Workaround for python 3.6.0 issue -- # Instead of throwing ValueError if year out of range for datetime, # Python 3.6 will do some weird behavior that masks high bits # e.g. (1<<40) -> year 36812, but (1<<41) -> year 6118. # (Appears to be bug http://bugs.python.org/issue29100) # This check stops at largest non-wrapping bit size. if next_year < year: break value = next_value # 'value-1' is maximum. value -= 1 # check for crazy case where we're beyond what datetime supports # (caused by bug 29100 again). compare to max value that datetime # module supports -- datetime.datetime(9999, 12, 31, 23, 59, 59, 999999) max_datetime_timestamp = 253402318800 return min(value, max_datetime_timestamp) #: Rough approximation of max value acceptable by hosts's time_t. #: This is frequently ~2**37 on 64 bit, and ~2**31 on 32 bit systems. max_time_t = _get_max_time_t() def to_b32_size(raw_size): return (raw_size * 8 + 4) // 5 #============================================================================= # wallet #============================================================================= class AppWalletTest(TestCase): descriptionPrefix = "passlib.totp.AppWallet" #============================================================================= # constructor #============================================================================= def test_secrets_types(self): """constructor -- 'secrets' param -- input types""" # no secrets wallet = AppWallet() self.assertEqual(wallet._secrets, {}) self.assertFalse(wallet.has_secrets) # dict ref = {"1": b"aaa", "2": b"bbb"} wallet = AppWallet(ref) self.assertEqual(wallet._secrets, ref) self.assertTrue(wallet.has_secrets) # # list # wallet = AppWallet(list(ref.items())) # self.assertEqual(wallet._secrets, ref) # # iter # wallet = AppWallet(iter(ref.items())) # self.assertEqual(wallet._secrets, ref) # "tag:value" string wallet = AppWallet("\n 1: aaa\n# comment\n \n2: bbb ") self.assertEqual(wallet._secrets, ref) # ensure ":" allowed in secret wallet = AppWallet("1: aaa: bbb \n# comment\n \n2: bbb ") self.assertEqual(wallet._secrets, {"1": b"aaa: bbb", "2": b"bbb"}) # json dict wallet = AppWallet('{"1":"aaa","2":"bbb"}') self.assertEqual(wallet._secrets, ref) # # json list # wallet = AppWallet('[["1","aaa"],["2","bbb"]]') # self.assertEqual(wallet._secrets, ref) # invalid type self.assertRaises(TypeError, AppWallet, 123) # invalid json obj self.assertRaises(TypeError, AppWallet, "[123]") # # invalid list items # self.assertRaises(ValueError, AppWallet, ["1", b"aaa"]) # forbid empty secret self.assertRaises(ValueError, AppWallet, {"1": "aaa", "2": ""}) def test_secrets_tags(self): """constructor -- 'secrets' param -- tag/value normalization""" # test reference ref = {"1": b"aaa", "02": b"bbb", "C": b"ccc"} wallet = AppWallet(ref) self.assertEqual(wallet._secrets, ref) # accept unicode wallet = AppWallet({u("1"): b"aaa", u("02"): b"bbb", u("C"): b"ccc"}) self.assertEqual(wallet._secrets, ref) # normalize int tags wallet = AppWallet({1: b"aaa", "02": b"bbb", "C": b"ccc"}) self.assertEqual(wallet._secrets, ref) # forbid non-str/int tags self.assertRaises(TypeError, AppWallet, {(1,): "aaa"}) # accept valid tags wallet = AppWallet({"1-2_3.4": b"aaa"}) # forbid invalid tags self.assertRaises(ValueError, AppWallet, {"-abc": "aaa"}) self.assertRaises(ValueError, AppWallet, {"ab*$": "aaa"}) # coerce value to bytes wallet = AppWallet({"1": u("aaa"), "02": "bbb", "C": b"ccc"}) self.assertEqual(wallet._secrets, ref) # forbid invalid value types self.assertRaises(TypeError, AppWallet, {"1": 123}) self.assertRaises(TypeError, AppWallet, {"1": None}) self.assertRaises(TypeError, AppWallet, {"1": []}) # TODO: test secrets_path def test_default_tag(self): """constructor -- 'default_tag' param""" # should sort numerically wallet = AppWallet({"1": "one", "02": "two"}) self.assertEqual(wallet.default_tag, "02") self.assertEqual(wallet.get_secret(wallet.default_tag), b"two") # should sort alphabetically if non-digit present wallet = AppWallet({"1": "one", "02": "two", "A": "aaa"}) self.assertEqual(wallet.default_tag, "A") self.assertEqual(wallet.get_secret(wallet.default_tag), b"aaa") # should use honor custom tag wallet = AppWallet({"1": "one", "02": "two", "A": "aaa"}, default_tag="1") self.assertEqual(wallet.default_tag, "1") self.assertEqual(wallet.get_secret(wallet.default_tag), b"one") # throw error on unknown value self.assertRaises(KeyError, AppWallet, {"1": "one", "02": "two", "A": "aaa"}, default_tag="B") # should be empty wallet = AppWallet() self.assertEqual(wallet.default_tag, None) self.assertRaises(KeyError, wallet.get_secret, None) # TODO: test 'cost' param #============================================================================= # encrypt_key() & decrypt_key() helpers #============================================================================= def require_aes_support(self, canary=None): if AES_SUPPORT: canary and canary() else: canary and self.assertRaises(RuntimeError, canary) raise self.skipTest("'cryptography' package not installed") def test_decrypt_key(self): """.decrypt_key()""" wallet = AppWallet({"1": PASS1, "2": PASS2}) # check for support CIPHER1 = dict(v=1, c=13, s='6D7N7W53O7HHS37NLUFQ', k='MHCTEGSNPFN5CGBJ', t='1') self.require_aes_support(canary=partial(wallet.decrypt_key, CIPHER1)) # reference key self.assertEqual(wallet.decrypt_key(CIPHER1)[0], KEY1_RAW) # different salt used to encrypt same raw key CIPHER2 = dict(v=1, c=13, s='SPZJ54Y6IPUD2BYA4C6A', k='ZGDXXTVQOWYLC2AU', t='1') self.assertEqual(wallet.decrypt_key(CIPHER2)[0], KEY1_RAW) # different sized key, password, and cost CIPHER3 = dict(v=1, c=8, s='FCCTARTIJWE7CPQHUDKA', k='D2DRS32YESGHHINWFFCELKN7Z6NAHM4M', t='2') self.assertEqual(wallet.decrypt_key(CIPHER3)[0], KEY2_RAW) # wrong password should silently result in wrong key temp = CIPHER1.copy() temp.update(t='2') self.assertEqual(wallet.decrypt_key(temp)[0], b'\xafD6.F7\xeb\x19\x05Q') # missing tag should throw error temp = CIPHER1.copy() temp.update(t='3') self.assertRaises(KeyError, wallet.decrypt_key, temp) # unknown version should throw error temp = CIPHER1.copy() temp.update(v=999) self.assertRaises(ValueError, wallet.decrypt_key, temp) def test_decrypt_key_needs_recrypt(self): """.decrypt_key() -- needs_recrypt flag""" self.require_aes_support() wallet = AppWallet({"1": PASS1, "2": PASS2}, encrypt_cost=13) # ref should be accepted ref = dict(v=1, c=13, s='AAAA', k='AAAA', t='2') self.assertFalse(wallet.decrypt_key(ref)[1]) # wrong cost temp = ref.copy() temp.update(c=8) self.assertTrue(wallet.decrypt_key(temp)[1]) # wrong tag temp = ref.copy() temp.update(t="1") self.assertTrue(wallet.decrypt_key(temp)[1]) # XXX: should this check salt_size? def assertSaneResult(self, result, wallet, key, tag="1", needs_recrypt=False): """check encrypt_key() result has expected format""" self.assertEqual(set(result), set(["v", "t", "c", "s", "k"])) self.assertEqual(result['v'], 1) self.assertEqual(result['t'], tag) self.assertEqual(result['c'], wallet.encrypt_cost) self.assertEqual(len(result['s']), to_b32_size(wallet.salt_size)) self.assertEqual(len(result['k']), to_b32_size(len(key))) result_key, result_needs_recrypt = wallet.decrypt_key(result) self.assertEqual(result_key, key) self.assertEqual(result_needs_recrypt, needs_recrypt) def test_encrypt_key(self): """.encrypt_key()""" # check for support wallet = AppWallet({"1": PASS1}, encrypt_cost=5) self.require_aes_support(canary=partial(wallet.encrypt_key, KEY1_RAW)) # basic behavior result = wallet.encrypt_key(KEY1_RAW) self.assertSaneResult(result, wallet, KEY1_RAW) # creates new salt each time other = wallet.encrypt_key(KEY1_RAW) self.assertSaneResult(result, wallet, KEY1_RAW) self.assertNotEqual(other['s'], result['s']) self.assertNotEqual(other['k'], result['k']) # honors custom cost wallet2 = AppWallet({"1": PASS1}, encrypt_cost=6) result = wallet2.encrypt_key(KEY1_RAW) self.assertSaneResult(result, wallet2, KEY1_RAW) # honors default tag wallet2 = AppWallet({"1": PASS1, "2": PASS2}) result = wallet2.encrypt_key(KEY1_RAW) self.assertSaneResult(result, wallet2, KEY1_RAW, tag="2") # honor salt size wallet2 = AppWallet({"1": PASS1}) wallet2.salt_size = 64 result = wallet2.encrypt_key(KEY1_RAW) self.assertSaneResult(result, wallet2, KEY1_RAW) # larger key result = wallet.encrypt_key(KEY2_RAW) self.assertSaneResult(result, wallet, KEY2_RAW) # border case: empty key # XXX: might want to allow this, but documenting behavior for now self.assertRaises(ValueError, wallet.encrypt_key, b"") def test_encrypt_cost_timing(self): """verify cost parameter via timing""" self.require_aes_support() # time default cost wallet = AppWallet({"1": "aaa"}) wallet.encrypt_cost -= 2 delta, _ = time_call(partial(wallet.encrypt_key, KEY1_RAW), maxtime=0) # this should take (2**3=8) times as long wallet.encrypt_cost += 3 delta2, _ = time_call(partial(wallet.encrypt_key, KEY1_RAW), maxtime=0) self.assertAlmostEqual(delta2, delta*8, delta=(delta*8)*0.5) #============================================================================= # eoc #============================================================================= #============================================================================= # common OTP code #============================================================================= #: used as base value for RFC test vector keys RFC_KEY_BYTES_20 = "12345678901234567890".encode("ascii") RFC_KEY_BYTES_32 = (RFC_KEY_BYTES_20*2)[:32] RFC_KEY_BYTES_64 = (RFC_KEY_BYTES_20*4)[:64] # TODO: this class is separate from TotpTest due to historical issue, # when there was a base class, and a separate HOTP class. # these test case classes should probably be combined. class TotpTest(TestCase): """ common code shared by TotpTest & HotpTest """ #============================================================================= # class attrs #============================================================================= descriptionPrefix = "passlib.totp.TOTP" #============================================================================= # setup #============================================================================= def setUp(self): super(TotpTest, self).setUp() # clear norm_hash_name() cache so 'unknown hash' warnings get emitted each time from passlib.crypto.digest import lookup_hash lookup_hash.clear_cache() # monkeypatch module's rng to be deterministic self.patchAttr(totp_module, "rng", self.getRandom()) #============================================================================= # general helpers #============================================================================= def randtime(self): """ helper to generate random epoch time :returns float: epoch time """ return self.getRandom().random() * max_time_t def randotp(self, cls=None, **kwds): """ helper which generates a random TOTP instance. """ rng = self.getRandom() if "key" not in kwds: kwds['new'] = True kwds.setdefault("digits", rng.randint(6, 10)) kwds.setdefault("alg", rng.choice(["sha1", "sha256", "sha512"])) kwds.setdefault("period", rng.randint(10, 120)) return (cls or TOTP)(**kwds) def test_randotp(self): """ internal test -- randotp() """ otp1 = self.randotp() otp2 = self.randotp() self.assertNotEqual(otp1.key, otp2.key, "key not randomized:") # NOTE: has (1/5)**10 odds of failure for _ in range(10): if otp1.digits != otp2.digits: break otp2 = self.randotp() else: self.fail("digits not randomized") # NOTE: has (1/3)**10 odds of failure for _ in range(10): if otp1.alg != otp2.alg: break otp2 = self.randotp() else: self.fail("alg not randomized") #============================================================================= # reference vector helpers #============================================================================= #: default options used by test vectors (unless otherwise stated) vector_defaults = dict(format="base32", alg="sha1", period=30, digits=8) #: various TOTP test vectors, #: each element in list has format [options, (time, token <, int(expires)>), ...] vectors = [ #------------------------------------------------------------------------- # passlib test vectors #------------------------------------------------------------------------- # 10 byte key, 6 digits [dict(key="ACDEFGHJKL234567", digits=6), # test fencepost to make sure we're rounding right (1412873399, '221105'), # == 29 mod 30 (1412873400, '178491'), # == 0 mod 30 (1412873401, '178491'), # == 1 mod 30 (1412873429, '178491'), # == 29 mod 30 (1412873430, '915114'), # == 0 mod 30 ], # 10 byte key, 8 digits [dict(key="ACDEFGHJKL234567", digits=8), # should be same as 6 digits (above), but w/ 2 more digits on left side of token. (1412873399, '20221105'), # == 29 mod 30 (1412873400, '86178491'), # == 0 mod 30 (1412873401, '86178491'), # == 1 mod 30 (1412873429, '86178491'), # == 29 mod 30 (1412873430, '03915114'), # == 0 mod 30 ], # sanity check on key used in docstrings [dict(key="S3JD-VB7Q-D2R7-JPXX", digits=6), (1419622709, '000492'), (1419622739, '897212'), ], #------------------------------------------------------------------------- # reference vectors taken from http://tools.ietf.org/html/rfc6238, appendix B # NOTE: while appendix B states same key used for all tests, the reference # code in the appendix repeats the key up to the alg's block size, # and uses *that* as the secret... so that's what we're doing here. #------------------------------------------------------------------------- # sha1 test vectors [dict(key=RFC_KEY_BYTES_20, format="raw", alg="sha1"), (59, '94287082'), (1111111109, '07081804'), (1111111111, '14050471'), (1234567890, '89005924'), (2000000000, '69279037'), (20000000000, '65353130'), ], # sha256 test vectors [dict(key=RFC_KEY_BYTES_32, format="raw", alg="sha256"), (59, '46119246'), (1111111109, '68084774'), (1111111111, '67062674'), (1234567890, '91819424'), (2000000000, '90698825'), (20000000000, '77737706'), ], # sha512 test vectors [dict(key=RFC_KEY_BYTES_64, format="raw", alg="sha512"), (59, '90693936'), (1111111109, '25091201'), (1111111111, '99943326'), (1234567890, '93441116'), (2000000000, '38618901'), (20000000000, '47863826'), ], #------------------------------------------------------------------------- # other test vectors #------------------------------------------------------------------------- # generated at http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript [dict(key="JBSWY3DPEHPK3PXP", digits=6), (1409192430, '727248'), (1419890990, '122419')], [dict(key="JBSWY3DPEHPK3PXP", digits=9, period=41), (1419891152, '662331049')], # found in https://github.com/eloquent/otis/blob/develop/test/suite/Totp/Value/TotpValueGeneratorTest.php, line 45 [dict(key=RFC_KEY_BYTES_20, format="raw", period=60), (1111111111, '19360094')], [dict(key=RFC_KEY_BYTES_32, format="raw", alg="sha256", period=60), (1111111111, '40857319')], [dict(key=RFC_KEY_BYTES_64, format="raw", alg="sha512", period=60), (1111111111, '37023009')], ] def iter_test_vectors(self): """ helper to iterate over test vectors. yields ``(totp, time, token, expires, prefix)`` tuples. """ from passlib.totp import TOTP for row in self.vectors: kwds = self.vector_defaults.copy() kwds.update(row[0]) for entry in row[1:]: if len(entry) == 3: time, token, expires = entry else: time, token = entry expires = None # NOTE: not re-using otp between calls so that stateful methods # (like .match) don't have problems. log.debug("test vector: %r time=%r token=%r expires=%r", kwds, time, token, expires) otp = TOTP(**kwds) prefix = "alg=%r time=%r token=%r: " % (otp.alg, time, token) yield otp, time, token, expires, prefix #============================================================================= # constructor tests #============================================================================= def test_ctor_w_new(self): """constructor -- 'new' parameter""" # exactly one of 'key' or 'new' is required self.assertRaises(TypeError, TOTP) self.assertRaises(TypeError, TOTP, key='4aoggdbbqsyhntuz', new=True) # generates new key otp = TOTP(new=True) otp2 = TOTP(new=True) self.assertNotEqual(otp.key, otp2.key) def test_ctor_w_size(self): """constructor -- 'size' parameter""" # should default to digest size, per RFC self.assertEqual(len(TOTP(new=True, alg="sha1").key), 20) self.assertEqual(len(TOTP(new=True, alg="sha256").key), 32) self.assertEqual(len(TOTP(new=True, alg="sha512").key), 64) # explicit key size self.assertEqual(len(TOTP(new=True, size=10).key), 10) self.assertEqual(len(TOTP(new=True, size=16).key), 16) # for new=True, maximum size enforced (based on alg) self.assertRaises(ValueError, TOTP, new=True, size=21, alg="sha1") # for new=True, minimum size enforced self.assertRaises(ValueError, TOTP, new=True, size=9) # for existing key, minimum size is only warned about with self.assertWarningList([ dict(category=exc.PasslibSecurityWarning, message_re=".*for security purposes, secret key must be.*") ]): _ = TOTP('0A'*9, 'hex') def test_ctor_w_key_and_format(self): """constructor -- 'key' and 'format' parameters""" # handle base32 encoding (the default) self.assertEqual(TOTP(KEY1).key, KEY1_RAW) # .. w/ lower case self.assertEqual(TOTP(KEY1.lower()).key, KEY1_RAW) # .. w/ spaces (e.g. user-entered data) self.assertEqual(TOTP(' 4aog gdbb qsyh ntuz ').key, KEY1_RAW) # .. w/ invalid char self.assertRaises(Base32DecodeError, TOTP, 'ao!ggdbbqsyhntuz') # handle hex encoding self.assertEqual(TOTP('e01c630c2184b076ce99', 'hex').key, KEY1_RAW) # .. w/ invalid char self.assertRaises(Base16DecodeError, TOTP, 'X01c630c2184b076ce99', 'hex') # handle raw bytes self.assertEqual(TOTP(KEY1_RAW, "raw").key, KEY1_RAW) def test_ctor_w_alg(self): """constructor -- 'alg' parameter""" # normalize hash names self.assertEqual(TOTP(KEY1, alg="SHA-256").alg, "sha256") self.assertEqual(TOTP(KEY1, alg="SHA256").alg, "sha256") # invalid alg self.assertRaises(ValueError, TOTP, KEY1, alg="SHA-333") def test_ctor_w_digits(self): """constructor -- 'digits' parameter""" self.assertRaises(ValueError, TOTP, KEY1, digits=5) self.assertEqual(TOTP(KEY1, digits=6).digits, 6) # min value self.assertEqual(TOTP(KEY1, digits=10).digits, 10) # max value self.assertRaises(ValueError, TOTP, KEY1, digits=11) def test_ctor_w_period(self): """constructor -- 'period' parameter""" # default self.assertEqual(TOTP(KEY1).period, 30) # explicit value self.assertEqual(TOTP(KEY1, period=63).period, 63) # reject wrong type self.assertRaises(TypeError, TOTP, KEY1, period=1.5) self.assertRaises(TypeError, TOTP, KEY1, period='abc') # reject non-positive values self.assertRaises(ValueError, TOTP, KEY1, period=0) self.assertRaises(ValueError, TOTP, KEY1, period=-1) def test_ctor_w_label(self): """constructor -- 'label' parameter""" self.assertEqual(TOTP(KEY1).label, None) self.assertEqual(TOTP(KEY1, label="foo@bar").label, "foo@bar") self.assertRaises(ValueError, TOTP, KEY1, label="foo:bar") def test_ctor_w_issuer(self): """constructor -- 'issuer' parameter""" self.assertEqual(TOTP(KEY1).issuer, None) self.assertEqual(TOTP(KEY1, issuer="foo.com").issuer, "foo.com") self.assertRaises(ValueError, TOTP, KEY1, issuer="foo.com:bar") #============================================================================= # using() tests #============================================================================= # TODO: test using() w/ 'digits', 'alg', 'issue', 'wallet', **wallet_kwds def test_using_w_period(self): """using() -- 'period' parameter""" # default self.assertEqual(TOTP(KEY1).period, 30) # explicit value self.assertEqual(TOTP.using(period=63)(KEY1).period, 63) # reject wrong type self.assertRaises(TypeError, TOTP.using, period=1.5) self.assertRaises(TypeError, TOTP.using, period='abc') # reject non-positive values self.assertRaises(ValueError, TOTP.using, period=0) self.assertRaises(ValueError, TOTP.using, period=-1) def test_using_w_now(self): """using -- 'now' parameter""" # NOTE: reading time w/ normalize_time() to make sure custom .now actually has effect. # default -- time.time otp = self.randotp() self.assertIs(otp.now, _time.time) self.assertAlmostEqual(otp.normalize_time(None), int(_time.time())) # custom function counter = [123.12] def now(): counter[0] += 1 return counter[0] otp = self.randotp(cls=TOTP.using(now=now)) # NOTE: TOTP() constructor invokes this as part of test, using up counter values 124 & 125 self.assertEqual(otp.normalize_time(None), 126) self.assertEqual(otp.normalize_time(None), 127) # require callable self.assertRaises(TypeError, TOTP.using, now=123) # require returns int/float msg_re = r"now\(\) function must return non-negative" self.assertRaisesRegex(AssertionError, msg_re, TOTP.using, now=lambda: 'abc') # require returns non-negative value self.assertRaisesRegex(AssertionError, msg_re, TOTP.using, now=lambda: -1) #============================================================================= # internal method tests #============================================================================= def test_normalize_token_instance(self, otp=None): """normalize_token() -- instance method""" if otp is None: otp = self.randotp(digits=7) # unicode & bytes self.assertEqual(otp.normalize_token(u('1234567')), '1234567') self.assertEqual(otp.normalize_token(b'1234567'), '1234567') # int self.assertEqual(otp.normalize_token(1234567), '1234567') # int which needs 0 padding self.assertEqual(otp.normalize_token(234567), '0234567') # reject wrong types (float, None) self.assertRaises(TypeError, otp.normalize_token, 1234567.0) self.assertRaises(TypeError, otp.normalize_token, None) # too few digits self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '123456') # too many digits self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '01234567') self.assertRaises(exc.MalformedTokenError, otp.normalize_token, 12345678) def test_normalize_token_class(self): """normalize_token() -- class method""" self.test_normalize_token_instance(otp=TOTP.using(digits=7)) def test_normalize_time(self): """normalize_time()""" TotpFactory = TOTP.using() otp = self.randotp(TotpFactory) for _ in range(10): time = self.randtime() tint = int(time) self.assertEqual(otp.normalize_time(time), tint) self.assertEqual(otp.normalize_time(tint + 0.5), tint) self.assertEqual(otp.normalize_time(tint), tint) dt = datetime.datetime.utcfromtimestamp(time) self.assertEqual(otp.normalize_time(dt), tint) orig = TotpFactory.now try: TotpFactory.now = staticmethod(lambda: time) self.assertEqual(otp.normalize_time(None), tint) finally: TotpFactory.now = orig self.assertRaises(TypeError, otp.normalize_time, '1234') #============================================================================= # key attr tests #============================================================================= def test_key_attrs(self): """pretty_key() and .key attributes""" rng = self.getRandom() # test key attrs otp = TOTP(KEY1_RAW, "raw") self.assertEqual(otp.key, KEY1_RAW) self.assertEqual(otp.hex_key, 'e01c630c2184b076ce99') self.assertEqual(otp.base32_key, KEY1) # test pretty_key() self.assertEqual(otp.pretty_key(), '4AOG-GDBB-QSYH-NTUZ') self.assertEqual(otp.pretty_key(sep=" "), '4AOG GDBB QSYH NTUZ') self.assertEqual(otp.pretty_key(sep=False), KEY1) self.assertEqual(otp.pretty_key(format="hex"), 'e01c-630c-2184-b076-ce99') # quick fuzz test: make attr access works for random key & random size otp = TOTP(new=True, size=rng.randint(10, 20)) _ = otp.hex_key _ = otp.base32_key _ = otp.pretty_key() #============================================================================= # generate() tests #============================================================================= def test_totp_token(self): """generate() -- TotpToken() class""" from passlib.totp import TOTP, TotpToken # test known set of values otp = TOTP('s3jdvb7qd2r7jpxx') result = otp.generate(1419622739) self.assertIsInstance(result, TotpToken) self.assertEqual(result.token, '897212') self.assertEqual(result.counter, 47320757) ##self.assertEqual(result.start_time, 1419622710) self.assertEqual(result.expire_time, 1419622740) self.assertEqual(result, ('897212', 1419622740)) self.assertEqual(len(result), 2) self.assertEqual(result[0], '897212') self.assertEqual(result[1], 1419622740) self.assertRaises(IndexError, result.__getitem__, -3) self.assertRaises(IndexError, result.__getitem__, 2) self.assertTrue(result) # time dependant bits... otp.now = lambda : 1419622739.5 self.assertEqual(result.remaining, 0.5) self.assertTrue(result.valid) otp.now = lambda : 1419622741 self.assertEqual(result.remaining, 0) self.assertFalse(result.valid) # same time -- shouldn't return same object, but should be equal result2 = otp.generate(1419622739) self.assertIsNot(result2, result) self.assertEqual(result2, result) # diff time in period -- shouldn't return same object, but should be equal result3 = otp.generate(1419622711) self.assertIsNot(result3, result) self.assertEqual(result3, result) # shouldn't be equal result4 = otp.generate(1419622999) self.assertNotEqual(result4, result) def test_generate(self): """generate()""" from passlib.totp import TOTP # generate token otp = TOTP(new=True) time = self.randtime() result = otp.generate(time) token = result.token self.assertIsInstance(token, unicode) start_time = result.counter * 30 # should generate same token for next 29s self.assertEqual(otp.generate(start_time + 29).token, token) # and new one at 30s self.assertNotEqual(otp.generate(start_time + 30).token, token) # verify round-trip conversion of datetime dt = datetime.datetime.utcfromtimestamp(time) self.assertEqual(int(otp.normalize_time(dt)), int(time)) # handle datetime object self.assertEqual(otp.generate(dt).token, token) # omitting value should use current time otp2 = TOTP.using(now=lambda: time)(key=otp.base32_key) self.assertEqual(otp2.generate().token, token) # reject invalid time self.assertRaises(ValueError, otp.generate, -1) def test_generate_w_reference_vectors(self): """generate() -- reference vectors""" for otp, time, token, expires, prefix in self.iter_test_vectors(): # should output correct token for specified time result = otp.generate(time) self.assertEqual(result.token, token, msg=prefix) self.assertEqual(result.counter, time // otp.period, msg=prefix) if expires: self.assertEqual(result.expire_time, expires) #============================================================================= # TotpMatch() tests #============================================================================= def assertTotpMatch(self, match, time, skipped=0, period=30, window=30, msg=''): from passlib.totp import TotpMatch # test type self.assertIsInstance(match, TotpMatch) # totp sanity check self.assertIsInstance(match.totp, TOTP) self.assertEqual(match.totp.period, period) # test attrs self.assertEqual(match.time, time, msg=msg + " matched time:") expected = time // period counter = expected + skipped self.assertEqual(match.counter, counter, msg=msg + " matched counter:") self.assertEqual(match.expected_counter, expected, msg=msg + " expected counter:") self.assertEqual(match.skipped, skipped, msg=msg + " skipped:") self.assertEqual(match.cache_seconds, period + window) expire_time = (counter + 1) * period self.assertEqual(match.expire_time, expire_time) self.assertEqual(match.cache_time, expire_time + window) # test tuple self.assertEqual(len(match), 2) self.assertEqual(match, (counter, time)) self.assertRaises(IndexError, match.__getitem__, -3) self.assertEqual(match[0], counter) self.assertEqual(match[1], time) self.assertRaises(IndexError, match.__getitem__, 2) # test bool self.assertTrue(match) def test_totp_match_w_valid_token(self): """match() -- valid TotpMatch object""" time = 141230981 token = '781501' otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) result = otp.match(token, time) self.assertTotpMatch(result, time=time, skipped=0) def test_totp_match_w_older_token(self): """match() -- valid TotpMatch object with future token""" from passlib.totp import TotpMatch time = 141230981 token = '781501' otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) result = otp.match(token, time - 30) self.assertTotpMatch(result, time=time - 30, skipped=1) def test_totp_match_w_new_token(self): """match() -- valid TotpMatch object with past token""" time = 141230981 token = '781501' otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) result = otp.match(token, time + 30) self.assertTotpMatch(result, time=time + 30, skipped=-1) def test_totp_match_w_invalid_token(self): """match() -- invalid TotpMatch object""" time = 141230981 token = '781501' otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3) self.assertRaises(exc.InvalidTokenError, otp.match, token, time + 60) #============================================================================= # match() tests #============================================================================= def assertVerifyMatches(self, expect_skipped, token, time, # * otp, gen_time=None, **kwds): """helper to test otp.match() output is correct""" # NOTE: TotpMatch return type tested more throughly above ^^^ msg = "key=%r alg=%r period=%r token=%r gen_time=%r time=%r:" % \ (otp.base32_key, otp.alg, otp.period, token, gen_time, time) result = otp.match(token, time, **kwds) self.assertTotpMatch(result, time=otp.normalize_time(time), period=otp.period, window=kwds.get("window", 30), skipped=expect_skipped, msg=msg) def assertVerifyRaises(self, exc_class, token, time, # * otp, gen_time=None, **kwds): """helper to test otp.match() throws correct error""" # NOTE: TotpMatch return type tested more throughly above ^^^ msg = "key=%r alg=%r period=%r token=%r gen_time=%r time=%r:" % \ (otp.base32_key, otp.alg, otp.period, token, gen_time, time) return self.assertRaises(exc_class, otp.match, token, time, __msg__=msg, **kwds) def test_match_w_window(self): """match() -- 'time' and 'window' parameters""" # init generator & helper otp = self.randotp() period = otp.period time = self.randtime() token = otp.generate(time).token common = dict(otp=otp, gen_time=time) assertMatches = partial(self.assertVerifyMatches, **common) assertRaises = partial(self.assertVerifyRaises, **common) #------------------------------- # basic validation, and 'window' parameter #------------------------------- # validate against previous counter (passes if window >= period) assertRaises(exc.InvalidTokenError, token, time - period, window=0) assertMatches(+1, token, time - period, window=period) assertMatches(+1, token, time - period, window=2 * period) # validate against current counter assertMatches(0, token, time, window=0) # validate against next counter (passes if window >= period) assertRaises(exc.InvalidTokenError, token, time + period, window=0) assertMatches(-1, token, time + period, window=period) assertMatches(-1, token, time + period, window=2 * period) # validate against two time steps later (should never pass) assertRaises(exc.InvalidTokenError, token, time + 2 * period, window=0) assertRaises(exc.InvalidTokenError, token, time + 2 * period, window=period) assertMatches(-2, token, time + 2 * period, window=2 * period) # TODO: test window values that aren't multiples of period # (esp ensure counter rounding works correctly) #------------------------------- # time normalization #------------------------------- # handle datetimes dt = datetime.datetime.utcfromtimestamp(time) assertMatches(0, token, dt, window=0) # reject invalid time assertRaises(ValueError, token, -1) def test_match_w_skew(self): """match() -- 'skew' parameters""" # init generator & helper otp = self.randotp() period = otp.period time = self.randtime() common = dict(otp=otp, gen_time=time) assertMatches = partial(self.assertVerifyMatches, **common) assertRaises = partial(self.assertVerifyRaises, **common) # assume client is running far behind server / has excessive transmission delay skew = 3 * period behind_token = otp.generate(time - skew).token assertRaises(exc.InvalidTokenError, behind_token, time, window=0) assertMatches(-3, behind_token, time, window=0, skew=-skew) # assume client is running far ahead of server ahead_token = otp.generate(time + skew).token assertRaises(exc.InvalidTokenError, ahead_token, time, window=0) assertMatches(+3, ahead_token, time, window=0, skew=skew) # TODO: test skew + larger window def test_match_w_reuse(self): """match() -- 'reuse' and 'last_counter' parameters""" # init generator & helper otp = self.randotp() period = otp.period time = self.randtime() tdata = otp.generate(time) token = tdata.token counter = tdata.counter expire_time = tdata.expire_time common = dict(otp=otp, gen_time=time) assertMatches = partial(self.assertVerifyMatches, **common) assertRaises = partial(self.assertVerifyRaises, **common) # last counter unset -- # previous period's token should count as valid assertMatches(-1, token, time + period, window=period) # last counter set 2 periods ago -- # previous period's token should count as valid assertMatches(-1, token, time + period, last_counter=counter-1, window=period) # last counter set 2 periods ago -- # 2 periods ago's token should NOT count as valid assertRaises(exc.InvalidTokenError, token, time + 2 * period, last_counter=counter, window=period) # last counter set 1 period ago -- # previous period's token should now be rejected as 'used' err = assertRaises(exc.UsedTokenError, token, time + period, last_counter=counter, window=period) self.assertEqual(err.expire_time, expire_time) # last counter set to current period -- # current period's token should be rejected err = assertRaises(exc.UsedTokenError, token, time, last_counter=counter, window=0) self.assertEqual(err.expire_time, expire_time) def test_match_w_token_normalization(self): """match() -- token normalization""" # setup test helper otp = TOTP('otxl2f5cctbprpzx') match = otp.match time = 1412889861 # separators / spaces should be stripped (orig token '332136') self.assertTrue(match(' 3 32-136 ', time)) # ascii bytes self.assertTrue(match(b'332136', time)) # too few digits self.assertRaises(exc.MalformedTokenError, match, '12345', time) # invalid char self.assertRaises(exc.MalformedTokenError, match, '12345X', time) # leading zeros count towards size self.assertRaises(exc.MalformedTokenError, match, '0123456', time) def test_match_w_reference_vectors(self): """match() -- reference vectors""" for otp, time, token, expires, msg in self.iter_test_vectors(): # create wrapper match = otp.match # token should match against time result = match(token, time) self.assertTrue(result) self.assertEqual(result.counter, time // otp.period, msg=msg) # should NOT match against another time self.assertRaises(exc.InvalidTokenError, match, token, time + 100, window=0) #============================================================================= # verify() tests #============================================================================= def test_verify(self): """verify()""" # NOTE: since this is thin wrapper around .from_source() and .match(), # just testing basic behavior here. from passlib.totp import TOTP time = 1412889861 TotpFactory = TOTP.using(now=lambda: time) # successful match source1 = dict(v=1, type="totp", key='otxl2f5cctbprpzx') match = TotpFactory.verify('332136', source1) self.assertTotpMatch(match, time=time) # failed match source1 = dict(v=1, type="totp", key='otxl2f5cctbprpzx') self.assertRaises(exc.InvalidTokenError, TotpFactory.verify, '332155', source1) # bad source source1 = dict(v=1, type="totp") self.assertRaises(ValueError, TotpFactory.verify, '332155', source1) # successful match -- json source source1json = '{"v": 1, "type": "totp", "key": "otxl2f5cctbprpzx"}' match = TotpFactory.verify('332136', source1json) self.assertTotpMatch(match, time=time) # successful match -- URI source1uri = 'otpauth://totp/Label?secret=otxl2f5cctbprpzx' match = TotpFactory.verify('332136', source1uri) self.assertTotpMatch(match, time=time) #============================================================================= # serialization frontend tests #============================================================================= def test_from_source(self): """from_source()""" from passlib.totp import TOTP from_source = TOTP.from_source # uri (unicode) otp = from_source(u("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" "issuer=Example")) self.assertEqual(otp.key, KEY4_RAW) # uri (bytes) otp = from_source(b"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" b"issuer=Example") self.assertEqual(otp.key, KEY4_RAW) # dict otp = from_source(dict(v=1, type="totp", key=KEY4)) self.assertEqual(otp.key, KEY4_RAW) # json (unicode) otp = from_source(u('{"v": 1, "type": "totp", "key": "JBSWY3DPEHPK3PXP"}')) self.assertEqual(otp.key, KEY4_RAW) # json (bytes) otp = from_source(b'{"v": 1, "type": "totp", "key": "JBSWY3DPEHPK3PXP"}') self.assertEqual(otp.key, KEY4_RAW) # TOTP object -- return unchanged self.assertIs(from_source(otp), otp) # TOTP object w/ different wallet -- return new one. wallet1 = AppWallet() otp1 = TOTP.using(wallet=wallet1).from_source(otp) self.assertIsNot(otp1, otp) self.assertEqual(otp1.to_dict(), otp.to_dict()) # TOTP object w/ same wallet -- return original otp2 = TOTP.using(wallet=wallet1).from_source(otp1) self.assertIs(otp2, otp1) # random string self.assertRaises(ValueError, from_source, u("foo")) self.assertRaises(ValueError, from_source, b"foo") #============================================================================= # uri serialization tests #============================================================================= def test_from_uri(self): """from_uri()""" from passlib.totp import TOTP from_uri = TOTP.from_uri # URIs from https://code.google.com/p/google-authenticator/wiki/KeyUriFormat #-------------------------------------------------------------------------------- # canonical uri #-------------------------------------------------------------------------------- otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" "issuer=Example") self.assertIsInstance(otp, TOTP) self.assertEqual(otp.key, KEY4_RAW) self.assertEqual(otp.label, "alice@google.com") self.assertEqual(otp.issuer, "Example") self.assertEqual(otp.alg, "sha1") # default self.assertEqual(otp.period, 30) # default self.assertEqual(otp.digits, 6) # default #-------------------------------------------------------------------------------- # secret param #-------------------------------------------------------------------------------- # secret case insensitive otp = from_uri("otpauth://totp/Example:alice@google.com?secret=jbswy3dpehpk3pxp&" "issuer=Example") self.assertEqual(otp.key, KEY4_RAW) # missing secret self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?digits=6") # undecodable secret self.assertRaises(Base32DecodeError, from_uri, "otpauth://totp/Example:alice@google.com?" "secret=JBSWY3DPEHP@3PXP") #-------------------------------------------------------------------------------- # label param #-------------------------------------------------------------------------------- # w/ encoded space otp = from_uri("otpauth://totp/Provider1:Alice%20Smith?secret=JBSWY3DPEHPK3PXP&" "issuer=Provider1") self.assertEqual(otp.label, "Alice Smith") self.assertEqual(otp.issuer, "Provider1") # w/ encoded space and colon # (note url has leading space before 'alice') -- taken from KeyURI spec otp = from_uri("otpauth://totp/Big%20Corporation%3A%20alice@bigco.com?" "secret=JBSWY3DPEHPK3PXP") self.assertEqual(otp.label, "alice@bigco.com") self.assertEqual(otp.issuer, "Big Corporation") #-------------------------------------------------------------------------------- # issuer param / prefix #-------------------------------------------------------------------------------- # 'new style' issuer only otp = from_uri("otpauth://totp/alice@bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation") self.assertEqual(otp.label, "alice@bigco.com") self.assertEqual(otp.issuer, "Big Corporation") # new-vs-old issuer mismatch self.assertRaises(ValueError, TOTP.from_uri, "otpauth://totp/Provider1:alice?secret=JBSWY3DPEHPK3PXP&issuer=Provider2") #-------------------------------------------------------------------------------- # algorithm param #-------------------------------------------------------------------------------- # custom alg otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256") self.assertEqual(otp.alg, "sha256") # unknown alg self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?" "secret=JBSWY3DPEHPK3PXP&algorithm=SHA333") #-------------------------------------------------------------------------------- # digit param #-------------------------------------------------------------------------------- # custom digits otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=8") self.assertEqual(otp.digits, 8) # digits out of range / invalid self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=A") self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=%20") self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=15") #-------------------------------------------------------------------------------- # period param #-------------------------------------------------------------------------------- # custom period otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&period=63") self.assertEqual(otp.period, 63) # reject period < 1 self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?" "secret=JBSWY3DPEHPK3PXP&period=0") self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?" "secret=JBSWY3DPEHPK3PXP&period=-1") #-------------------------------------------------------------------------------- # unrecognized param #-------------------------------------------------------------------------------- # should issue warning, but otherwise ignore extra param with self.assertWarningList([ dict(category=exc.PasslibRuntimeWarning, message_re="unexpected parameters encountered") ]): otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&" "foo=bar&period=63") self.assertEqual(otp.base32_key, KEY4) self.assertEqual(otp.period, 63) def test_to_uri(self): """to_uri()""" #------------------------------------------------------------------------- # label & issuer parameters #------------------------------------------------------------------------- # with label & issuer otp = TOTP(KEY4, alg="sha1", digits=6, period=30) self.assertEqual(otp.to_uri("alice@google.com", "Example Org"), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" "issuer=Example%20Org") # label is required self.assertRaises(ValueError, otp.to_uri, None, "Example Org") # with label only self.assertEqual(otp.to_uri("alice@google.com"), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP") # with default label from constructor otp.label = "alice@google.com" self.assertEqual(otp.to_uri(), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP") # with default label & default issuer from constructor otp.issuer = "Example Org" self.assertEqual(otp.to_uri(), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP" "&issuer=Example%20Org") # reject invalid label self.assertRaises(ValueError, otp.to_uri, "label:with:semicolons") # reject invalid issue self.assertRaises(ValueError, otp.to_uri, "alice@google.com", "issuer:with:semicolons") #------------------------------------------------------------------------- # algorithm parameter #------------------------------------------------------------------------- self.assertEqual(TOTP(KEY4, alg="sha256").to_uri("alice@google.com"), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" "algorithm=SHA256") #------------------------------------------------------------------------- # digits parameter #------------------------------------------------------------------------- self.assertEqual(TOTP(KEY4, digits=8).to_uri("alice@google.com"), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" "digits=8") #------------------------------------------------------------------------- # period parameter #------------------------------------------------------------------------- self.assertEqual(TOTP(KEY4, period=63).to_uri("alice@google.com"), "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&" "period=63") #============================================================================= # dict serialization tests #============================================================================= def test_from_dict(self): """from_dict()""" from passlib.totp import TOTP from_dict = TOTP.from_dict #-------------------------------------------------------------------------------- # canonical simple example #-------------------------------------------------------------------------------- otp = from_dict(dict(v=1, type="totp", key=KEY4, label="alice@google.com", issuer="Example")) self.assertIsInstance(otp, TOTP) self.assertEqual(otp.key, KEY4_RAW) self.assertEqual(otp.label, "alice@google.com") self.assertEqual(otp.issuer, "Example") self.assertEqual(otp.alg, "sha1") # default self.assertEqual(otp.period, 30) # default self.assertEqual(otp.digits, 6) # default #-------------------------------------------------------------------------------- # metadata #-------------------------------------------------------------------------------- # missing version self.assertRaises(ValueError, from_dict, dict(type="totp", key=KEY4)) # invalid version self.assertRaises(ValueError, from_dict, dict(v=0, type="totp", key=KEY4)) self.assertRaises(ValueError, from_dict, dict(v=999, type="totp", key=KEY4)) # missing type self.assertRaises(ValueError, from_dict, dict(v=1, key=KEY4)) #-------------------------------------------------------------------------------- # secret param #-------------------------------------------------------------------------------- # secret case insensitive otp = from_dict(dict(v=1, type="totp", key=KEY4.lower(), label="alice@google.com", issuer="Example")) self.assertEqual(otp.key, KEY4_RAW) # missing secret self.assertRaises(ValueError, from_dict, dict(v=1, type="totp")) # undecodable secret self.assertRaises(Base32DecodeError, from_dict, dict(v=1, type="totp", key="JBSWY3DPEHP@3PXP")) #-------------------------------------------------------------------------------- # label & issuer params #-------------------------------------------------------------------------------- otp = from_dict(dict(v=1, type="totp", key=KEY4, label="Alice Smith", issuer="Provider1")) self.assertEqual(otp.label, "Alice Smith") self.assertEqual(otp.issuer, "Provider1") #-------------------------------------------------------------------------------- # algorithm param #-------------------------------------------------------------------------------- # custom alg otp = from_dict(dict(v=1, type="totp", key=KEY4, alg="sha256")) self.assertEqual(otp.alg, "sha256") # unknown alg self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, alg="sha333")) #-------------------------------------------------------------------------------- # digit param #-------------------------------------------------------------------------------- # custom digits otp = from_dict(dict(v=1, type="totp", key=KEY4, digits=8)) self.assertEqual(otp.digits, 8) # digits out of range / invalid self.assertRaises(TypeError, from_dict, dict(v=1, type="totp", key=KEY4, digits="A")) self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, digits=15)) #-------------------------------------------------------------------------------- # period param #-------------------------------------------------------------------------------- # custom period otp = from_dict(dict(v=1, type="totp", key=KEY4, period=63)) self.assertEqual(otp.period, 63) # reject period < 1 self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, period=0)) self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, period=-1)) #-------------------------------------------------------------------------------- # unrecognized param #-------------------------------------------------------------------------------- self.assertRaises(TypeError, from_dict, dict(v=1, type="totp", key=KEY4, INVALID=123)) def test_to_dict(self): """to_dict()""" #------------------------------------------------------------------------- # label & issuer parameters #------------------------------------------------------------------------- # without label or issuer otp = TOTP(KEY4, alg="sha1", digits=6, period=30) self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4)) # with label & issuer from constructor otp = TOTP(KEY4, alg="sha1", digits=6, period=30, label="alice@google.com", issuer="Example Org") self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4, label="alice@google.com", issuer="Example Org")) # with label only otp = TOTP(KEY4, alg="sha1", digits=6, period=30, label="alice@google.com") self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4, label="alice@google.com")) # with issuer only otp = TOTP(KEY4, alg="sha1", digits=6, period=30, issuer="Example Org") self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4, issuer="Example Org")) # don't serialize default issuer TotpFactory = TOTP.using(issuer="Example Org") otp = TotpFactory(KEY4) self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4)) # don't serialize default issuer *even if explicitly set* otp = TotpFactory(KEY4, issuer="Example Org") self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4)) #------------------------------------------------------------------------- # algorithm parameter #------------------------------------------------------------------------- self.assertEqual(TOTP(KEY4, alg="sha256").to_dict(), dict(v=1, type="totp", key=KEY4, alg="sha256")) #------------------------------------------------------------------------- # digits parameter #------------------------------------------------------------------------- self.assertEqual(TOTP(KEY4, digits=8).to_dict(), dict(v=1, type="totp", key=KEY4, digits=8)) #------------------------------------------------------------------------- # period parameter #------------------------------------------------------------------------- self.assertEqual(TOTP(KEY4, period=63).to_dict(), dict(v=1, type="totp", key=KEY4, period=63)) # TODO: to_dict() # with encrypt=False # with encrypt="auto" + wallet + secrets # with encrypt="auto" + wallet + no secrets # with encrypt="auto" + no wallet # with encrypt=True + wallet + secrets # with encrypt=True + wallet + no secrets # with encrypt=True + no wallet # that 'changed' is set for old versions, and old encryption tags. #============================================================================= # json serialization tests #============================================================================= # TODO: from_json() / to_json(). # (skipped for right now cause just wrapper for from_dict/to_dict) #============================================================================= # eoc #============================================================================= #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/_test_bad_register.py0000644000175000017500000000103512257351267023315 0ustar biscuitbiscuit00000000000000"""helper for method in test_registry.py""" from passlib.registry import register_crypt_handler import passlib.utils.handlers as uh class dummy_bad(uh.StaticHandler): name = "dummy_bad" class alt_dummy_bad(uh.StaticHandler): name = "dummy_bad" # NOTE: if passlib.tests is being run from symlink (e.g. via gaeunit), # this module may be imported a second time as test._test_bad_registry. # we don't want it to do anything in that case. if __name__.startswith("passlib.tests"): register_crypt_handler(alt_dummy_bad) passlib-1.7.1/passlib/tests/sample1.cfg0000644000175000017500000000036313015205366021130 0ustar biscuitbiscuit00000000000000[passlib] schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt default = md5_crypt all__vary_rounds = 0.1 bsdi_crypt__default_rounds = 25001 bsdi_crypt__max_rounds = 30001 sha512_crypt__max_rounds = 50000 sha512_crypt__min_rounds = 40000 passlib-1.7.1/passlib/tests/test_handlers_argon2.py0000644000175000017500000004106713015205366023574 0ustar biscuitbiscuit00000000000000"""passlib.tests.test_handlers_argon2 - tests for passlib hash algorithms""" #============================================================================= # imports #============================================================================= # core import logging log = logging.getLogger(__name__) import warnings # site # pkg from passlib import hash from passlib.tests.utils import HandlerCase, TEST_MODE from passlib.tests.test_handlers import UPASS_TABLE, PASS_TABLE_UTF8 # module #============================================================================= # a bunch of tests lifted nearlky verbatim from official argon2 UTs... # https://github.com/P-H-C/phc-winner-argon2/blob/master/src/test.c #============================================================================= def hashtest(version, t, logM, p, secret, salt, hex_digest, hash): return dict(version=version, rounds=t, logM=logM, memory_cost=1< max uint32 "$argon2i$v=19$m=65536,t=8589934592,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY", # unexpected param "$argon2i$v=19$m=65536,t=2,p=4,q=5$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY", # wrong param order "$argon2i$v=19$t=2,m=65536,p=4,q=5$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY", # constraint violation: m < 8 * p "$argon2i$v=19$m=127,t=2,p=16$c29tZXNhbHQ$IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4", ] def setUpWarnings(self): super(_base_argon2_test, self).setUpWarnings() warnings.filterwarnings("ignore", ".*Using argon2pure backend.*") def do_stub_encrypt(self, handler=None, **settings): if self.backend == "argon2_cffi": # overriding default since no way to get stub config from argon2._calc_hash() # (otherwise test_21b_max_rounds blocks trying to do max rounds) handler = (handler or self.handler).using(**settings) self = handler(use_defaults=True) self.checksum = self._stub_checksum assert self.checksum return self.to_string() else: return super(_base_argon2_test, self).do_stub_encrypt(handler, **settings) def test_03_legacy_hash_workflow(self): # override base method raise self.skipTest("legacy 1.6 workflow not supported") def test_keyid_parameter(self): # NOTE: keyid parameter currently not supported by official argon2 hash parser, # even though it's mentioned in the format spec. # we're trying to be consistent w/ this, so hashes w/ keyid should # always through a NotImplementedError. self.assertRaises(NotImplementedError, self.handler.verify, 'password', "$argon2i$v=19$m=65536,t=2,p=4,keyid=ABCD$c29tZXNhbHQ$" "IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4") def test_data_parameter(self): # NOTE: argon2 c library doesn't support passing in a data parameter to argon2_hash(); # but argon2_verify() appears to parse that info... but then discards it (!?). # not sure what proper behavior is, filed issue -- https://github.com/P-H-C/phc-winner-argon2/issues/143 # For now, replicating behavior we have for the two backends, to detect when things change. handler = self.handler # ref hash of 'password' when 'data' is correctly passed into argon2() sample1 = '$argon2i$v=19$m=512,t=2,p=2,data=c29tZWRhdGE$c29tZXNhbHQ$KgHyCesFyyjkVkihZ5VNFw' # ref hash of 'password' when 'data' is silently discarded (same digest as w/o data) sample2 = '$argon2i$v=19$m=512,t=2,p=2,data=c29tZWRhdGE$c29tZXNhbHQ$uEeXt1dxN1iFKGhklseW4w' # hash of 'password' w/o the data field sample3 = '$argon2i$v=19$m=512,t=2,p=2$c29tZXNhbHQ$uEeXt1dxN1iFKGhklseW4w' # # test sample 1 # if self.backend == "argon2_cffi": # argon2_cffi v16.1 would incorrectly return False here. # but v16.2 patches so it throws error on data parameter. # our code should detect that, and adapt it into a NotImplementedError self.assertRaises(NotImplementedError, handler.verify, "password", sample1) # incorrectly returns sample3, dropping data parameter self.assertEqual(handler.genhash("password", sample1), sample3) else: assert self.backend == "argon2pure" # should parse and verify self.assertTrue(handler.verify("password", sample1)) # should preserve sample1 self.assertEqual(handler.genhash("password", sample1), sample1) # # test sample 2 # if self.backend == "argon2_cffi": # argon2_cffi v16.1 would incorrectly return True here. # but v16.2 patches so it throws error on data parameter. # our code should detect that, and adapt it into a NotImplementedError self.assertRaises(NotImplementedError, handler.verify,"password", sample2) # incorrectly returns sample3, dropping data parameter self.assertEqual(handler.genhash("password", sample1), sample3) else: assert self.backend == "argon2pure" # should parse, but fail to verify self.assertFalse(self.handler.verify("password", sample2)) # should return sample1 (corrected digest) self.assertEqual(handler.genhash("password", sample2), sample1) def test_keyid_and_data_parameters(self): # test combination of the two, just in case self.assertRaises(NotImplementedError, self.handler.verify, 'stub', "$argon2i$v=19$m=65536,t=2,p=4,keyid=ABCD,data=EFGH$c29tZXNhbHQ$" "IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4") def test_needs_update_w_type(self): handler = self.handler hash = handler.hash("stub") self.assertFalse(handler.needs_update(hash)) hash2 = hash.replace("$argon2i$", "$argon2d$") self.assertTrue(handler.needs_update(hash2)) def test_needs_update_w_version(self): handler = self.handler.using(memory_cost=65536, time_cost=2, parallelism=4, digest_size=32) hash = ("$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$" "QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY") if handler.max_version == 0x10: self.assertFalse(handler.needs_update(hash)) else: self.assertTrue(handler.needs_update(hash)) def test_argon_byte_encoding(self): """verify we're using right base64 encoding for argon2""" handler = self.handler if handler.version != 0x13: # TODO: make this fatal, and add refs for other version. raise self.skipTest("handler uses wrong version for sample hashes") # 8 byte salt salt = b'somesalt' temp = handler.using(memory_cost=256, time_cost=2, parallelism=2, salt=salt, checksum_size=32) hash = temp.hash("password") self.assertEqual(hash, "$argon2i$v=19$m=256,t=2,p=2" "$c29tZXNhbHQ" "$T/XOJ2mh1/TIpJHfCdQan76Q5esCFVoT5MAeIM1Oq2E") # 16 byte salt salt = b'somesalt\x00\x00\x00\x00\x00\x00\x00\x00' temp = handler.using(memory_cost=256, time_cost=2, parallelism=2, salt=salt, checksum_size=32) hash = temp.hash("password") self.assertEqual(hash, "$argon2i$v=19$m=256,t=2,p=2" "$c29tZXNhbHQAAAAAAAAAAA" "$rqnbEp1/jFDUEKZZmw+z14amDsFqMDC53dIe57ZHD38") class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): settings_map = HandlerCase.FuzzHashGenerator.settings_map.copy() settings_map.update(memory_cost="random_memory_cost") def random_memory_cost(self): if self.test.backend == "argon2pure": return self.randintgauss(128, 384, 256, 128) else: return self.randintgauss(128, 32767, 16384, 4096) # TODO: fuzz parallelism, digest_size #----------------------------------------- # test suites for specific backends #----------------------------------------- class argon2_argon2_cffi_test(_base_argon2_test.create_backend_case("argon2_cffi")): # add some more test vectors that take too long under argon2pure known_correct_hashes = _base_argon2_test.known_correct_hashes + [ # # sample hashes from argon2 cffi package's unittests, # which in turn were generated by official argon2 cmdline tool. # # v1.2, type I, w/o a version tag ('password', "$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$" "QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY"), # v1.3, type I ('password', "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" "IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4"), # v1.3, type D ('password', "$argon2d$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" "cZn5d+rFh+ZfuRhm2iGUGgcrW5YLeM6q7L3vBsdmFA0"), # # custom # # ensure trailing null bytes handled correctly ('password\x00', "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$" "Vpzuc0v0SrP88LcVvmg+z5RoOYpMDKH/lt6O+CZabIQ"), ] # add reference hashes from argon2 clib tests known_correct_hashes.extend( (info['secret'], info['hash']) for info in reference_data if info['logM'] <= (18 if TEST_MODE("full") else 16) ) class argon2_argon2pure_test(_base_argon2_test.create_backend_case("argon2pure")): # XXX: setting max_threads at 1 to prevent argon2pure from using multiprocessing, # which causes big problems when testing under pypy. # would like a "pure_use_threads" option instead, to make it use multiprocessing.dummy instead. handler = hash.argon2.using(memory_cost=32, parallelism=2) # don't use multiprocessing for unittests, makes it a lot harder to ctrl-c # XXX: make this controlled by env var? handler.pure_use_threads = True # add reference hashes from argon2 clib tests known_correct_hashes = _base_argon2_test.known_correct_hashes[:] known_correct_hashes.extend( (info['secret'], info['hash']) for info in reference_data if info['logM'] < 16 ) class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator): def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.randintgauss(1, 3, 2, 1) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_handlers_bcrypt.py0000644000175000017500000005572313015205366023713 0ustar biscuitbiscuit00000000000000"""passlib.tests.test_handlers - tests for passlib hash algorithms""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import logging; log = logging.getLogger(__name__) import os import warnings # site # pkg from passlib import hash from passlib.handlers.bcrypt import IDENT_2, IDENT_2X from passlib.utils import repeat_string, to_bytes from passlib.utils.compat import irange from passlib.tests.utils import HandlerCase, TEST_MODE from passlib.tests.test_handlers import UPASS_TABLE # module #============================================================================= # bcrypt #============================================================================= class _bcrypt_test(HandlerCase): """base for BCrypt test cases""" handler = hash.bcrypt reduce_default_rounds = True fuzz_salts_need_bcrypt_repair = True has_os_crypt_fallback = False known_correct_hashes = [ # # from JTR 1.7.9 # ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'), ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'), ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'), ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'), ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'), # # test vectors from http://www.openwall.com/crypt v1.2 # note that this omits any hashes that depend on crypt_blowfish's # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password, # and any 2x hashes); and only contain hashes which are correct # under both crypt_blowfish 1.2 AND OpenBSD. # ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'), ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'), ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'), ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'), ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789chars after 72 are ignored', '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'), (b'\xa3', '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), (b'\xff\xa3345', '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'), (b'\xa3ab', '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'), (b'\xaa'*72 + b'chars after 72 are ignored as usual', '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'), (b'\xaa\x55'*36, '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'), (b'\x55\xaa\xff'*24, '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'), # keeping one of their 2y tests, because we are supporting that. (b'\xa3', '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'), # # bsd wraparound bug (fixed in 2b) # # NOTE: if backend is vulnerable, password will hash the same as '0'*72 # ("$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"), # rather than same as ("0123456789"*8)[:72] # 255 should be sufficient, but checking (("0123456789"*26)[:254], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), (("0123456789"*26)[:255], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), (("0123456789"*26)[:256], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), (("0123456789"*26)[:257], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'), # # from py-bcrypt tests # ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'), ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'), ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'), ('abcdefghijklmnopqrstuvwxyz', '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'), ('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'), # # custom test vectors # # ensures utf-8 used for unicode (UPASS_TABLE, '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), # ensure 2b support (UPASS_TABLE, '$2b$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'), ] if TEST_MODE("full"): # # add some extra tests related to 2/2a # CONFIG_2 = '$2$05$' + '.'*22 CONFIG_A = '$2a$05$' + '.'*22 known_correct_hashes.extend([ ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'), ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'), ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'), ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'), ]) known_correct_configs = [ ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE, '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'), ] known_unidentified_hashes = [ # invalid minor version "$2f$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", ] known_malformed_hashes = [ # bad char in otherwise correct hash # \/ "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", # unsupported (but recognized) minor version "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q", # rounds not zero-padded (py-bcrypt rejects this, therefore so do we) '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.' # NOTE: salts with padding bits set are technically malformed, # but we can reliably correct & issue a warning for that. ] platform_crypt_support = [ ("freedbsd|openbsd|netbsd", True), ("darwin", False), # linux - may be present via addon, e.g. debian's libpam-unix2 # solaris - depends on policy ] #=================================================================== # override some methods #=================================================================== def setUp(self): # ensure builtin is enabled for duration of test. if TEST_MODE("full") and self.backend == "builtin": key = "PASSLIB_BUILTIN_BCRYPT" orig = os.environ.get(key) if orig: self.addCleanup(os.environ.__setitem__, key, orig) else: self.addCleanup(os.environ.__delitem__, key) os.environ[key] = "true" super(_bcrypt_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_test, self).populate_settings(kwds) #=================================================================== # fuzz testing #=================================================================== def crypt_supports_variant(self, hash): """check if OS crypt is expected to support given ident""" from passlib.handlers.bcrypt import bcrypt, IDENT_2X, IDENT_2Y from passlib.utils import safe_crypt ident = bcrypt.from_string(hash) return (safe_crypt("test", ident + "04$5BJqKfqMQvV7nS.yUguNcu") or "").startswith(ident) fuzz_verifiers = HandlerCase.fuzz_verifiers + ( "fuzz_verifier_bcrypt", "fuzz_verifier_pybcrypt", "fuzz_verifier_bcryptor", ) def fuzz_verifier_bcrypt(self): # test against bcrypt, if available from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, _detect_pybcrypt from passlib.utils import to_native_str, to_bytes try: import bcrypt except ImportError: return if _detect_pybcrypt(): return def check_bcrypt(secret, hash): """bcrypt""" secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding) if hash.startswith(IDENT_2B): # bcrypt <1.1 lacks 2B support hash = IDENT_2A + hash[4:] elif hash.startswith(IDENT_2): # bcrypt 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) elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0": hash = IDENT_2B + hash[4:] hash = to_bytes(hash) try: return bcrypt.hashpw(secret, hash) == hash except ValueError: raise ValueError("bcrypt rejected hash: %r (secret=%r)" % (hash, secret)) return check_bcrypt def fuzz_verifier_pybcrypt(self): # test against py-bcrypt, if available from passlib.handlers.bcrypt import ( IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, _PyBcryptBackend, ) from passlib.utils import to_native_str loaded = _PyBcryptBackend._load_backend_mixin("pybcrypt", False) if not loaded: return from passlib.handlers.bcrypt import _pybcrypt as bcrypt_mod lock = _PyBcryptBackend._calc_lock # reuse threadlock workaround for pybcrypt 0.2 def check_pybcrypt(secret, hash): """pybcrypt""" secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding) if len(secret) > 200: # vulnerable to wraparound bug secret = secret[:200] if hash.startswith((IDENT_2B, IDENT_2Y)): hash = IDENT_2A + hash[4:] try: if lock: with lock: return bcrypt_mod.hashpw(secret, hash) == hash else: return bcrypt_mod.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.FuzzHashGenerator.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 class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def generate(self): opts = super(_bcrypt_test.FuzzHashGenerator, self).generate() secret = opts['secret'] other = opts['other'] settings = opts['settings'] ident = settings.get('ident') if ident == IDENT_2X: # 2x is just recognized, not supported. don't test with it. del settings['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. opts['secret'], opts['other'] = self.random_password_pair() return opts def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.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 hash() / genconfig() don't generate invalid salts anymore # def check_padding(hash): assert hash.startswith(("$2a$", "$2b$")) and len(hash) >= 28, \ "unexpectedly malformed hash: %r" % (hash,) 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.using(rounds=bcrypt.min_rounds).hash("bob")) # # 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, "$2b$05$" + "." * (22 + 31)) # # 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) # make sure verify() works correctly with good & bad hashes with self.assertWarningList([corr_desc]): self.assertTrue(bcrypt.verify(pwd, bad)) with self.assertWarningList([]): self.assertTrue(bcrypt.verify(pwd, good)) # make sure normhash() corrects bad hashes, leaves good unchanged with self.assertWarningList([corr_desc]): self.assertEqual(bcrypt.normhash(bad), good) with self.assertWarningList([]): self.assertEqual(bcrypt.normhash(good), good) # make sure normhash() leaves non-bcrypt hashes alone self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc") def test_needs_update_w_padding(self): """needs_update corrects bcrypt padding""" # NOTE: see padding test above for details about issue this detects bcrypt = self.handler.using(rounds=4) # PASS1 = "test" BAD1 = "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" GOOD1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS" self.assertTrue(bcrypt.needs_update(BAD1)) self.assertFalse(bcrypt.needs_update(GOOD1)) #=================================================================== # eoc #=================================================================== # create test cases for specific backends bcrypt_bcrypt_test = _bcrypt_test.create_backend_case("bcrypt") bcrypt_pybcrypt_test = _bcrypt_test.create_backend_case("pybcrypt") bcrypt_bcryptor_test = _bcrypt_test.create_backend_case("bcryptor") bcrypt_os_crypt_test = _bcrypt_test.create_backend_case("os_crypt") bcrypt_builtin_test = _bcrypt_test.create_backend_case("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 alt_safe_crypt_handler = hash.bcrypt has_os_crypt_fallback = True 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'), # ensure 2b support ("password", '$bcrypt-sha256$2b,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'), (UPASS_TABLE, '$bcrypt-sha256$2b,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'), # test >72 chars is hashed correctly -- under bcrypt these hash the same. # NOTE: test_60_truncate_size() handles this already, this is just for overkill :) (repeat_string("abc123",72), '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'), (repeat_string("abc123",72)+"qwr", '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'), (repeat_string("abc123",72)+"xyz", '$bcrypt-sha256$2b,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 #=================================================================== class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.randintgauss(5, 8, 6, 1) # create test cases for specific backends bcrypt_sha256_bcrypt_test = _bcrypt_sha256_test.create_backend_case("bcrypt") bcrypt_sha256_pybcrypt_test = _bcrypt_sha256_test.create_backend_case("pybcrypt") bcrypt_sha256_bcryptor_test = _bcrypt_sha256_test.create_backend_case("bcryptor") bcrypt_sha256_os_crypt_test = _bcrypt_sha256_test.create_backend_case("os_crypt") bcrypt_sha256_builtin_test = _bcrypt_sha256_test.create_backend_case("builtin") #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_handlers.py0000644000175000017500000017250213043701700022315 0ustar biscuitbiscuit00000000000000"""passlib.tests.test_handlers - tests for passlib hash algorithms""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core 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, UserHandlerMixin, 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 # handlers which support multiple backends, but don't have multi-backend tests. _omitted_backend_tests = ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"] #: modules where get_handler_case() should search for test cases. _handler_test_modules = [ "test_handlers", "test_handlers_argon2", "test_handlers_bcrypt", "test_handlers_cisco", "test_handlers_django", "test_handlers_pbkdf2", "test_handlers_scrypt", ] 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 scheme not in _omitted_backend_tests: # NOTE: will throw MissingBackendError if none are installed. backend = handler.get_backend() name = "%s_%s_test" % (scheme, backend) else: name = "%s_test" % scheme for module in _handler_test_modules: modname = "passlib.tests." + module __import__(modname) mod = sys.modules[modname] try: return getattr(mod, name) except AttributeError: pass raise KeyError("test case %r not found" % name) #: hashes which there may not be a backend available for, #: and get_handler_case() may (correctly) throw a MissingBackendError conditionally_available_hashes = ["argon2", "bcrypt", "bcrypt_sha256"] #============================================================================= # 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 test_77_fuzz_input(self, **kwds): # we want to generate even rounds to verify it's correct, but want to ignore warnings warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd.*") super(_bsdi_crypt_test, self).test_77_fuzz_input(**kwds) def test_needs_update_w_even_rounds(self): """needs_update() should flag even rounds""" handler = self.handler even_hash = '_Y/../cG0zkJa6LY6k4c' odd_hash = '_Z/..TgFg0/ptQtpAgws' secret = 'test' # don't issue warning self.assertTrue(handler.verify(secret, even_hash)) self.assertTrue(handler.verify(secret, odd_hash)) # *do* signal as needing updates self.assertTrue(handler.needs_update(even_hash)) self.assertFalse(handler.needs_update(odd_hash)) # new hashes shouldn't have even rounds new_hash = handler.hash("stub") self.assertFalse(handler.needs_update(new_hash)) # create test cases for specific backends bsdi_crypt_os_crypt_test = _bsdi_crypt_test.create_backend_case("os_crypt") bsdi_crypt_builtin_test = _bsdi_crypt_test.create_backend_case("builtin") #============================================================================= # crypt16 #============================================================================= class crypt16_test(HandlerCase): handler = hash.crypt16 # 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 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), ] # create test cases for specific backends des_crypt_os_crypt_test = _des_crypt_test.create_backend_case("os_crypt") des_crypt_builtin_test = _des_crypt_test.create_backend_case("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==") ] class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def random_password(self): # NOTE: this hash currently rejects the empty string. while True: pwd = super(ldap_plaintext_test.FuzzHashGenerator, self).random_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!', ] # create test cases for specific backends ldap_md5_crypt_os_crypt_test =_ldap_md5_crypt_test.create_backend_case("os_crypt") ldap_md5_crypt_builtin_test =_ldap_md5_crypt_test.create_backend_case("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") # create test cases for specific backends ldap_sha1_crypt_os_crypt_test = _ldap_sha1_crypt_test.create_backend_case("os_crypt") #============================================================================= # lanman #============================================================================= class lmhash_test(EncodingHandlerMixin, HandlerCase): handler = hash.lmhash 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), ] # create test cases for specific backends md5_crypt_os_crypt_test = _md5_crypt_test.create_backend_case("os_crypt") md5_crypt_builtin_test = _md5_crypt_test.create_backend_case("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_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_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) class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def accept_password_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'), ] #============================================================================= # 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', ] #============================================================================= # (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), ] # create test cases for specific backends sha1_crypt_os_crypt_test = _sha1_crypt_test.create_backend_case("os_crypt") sha1_crypt_builtin_test = _sha1_crypt_test.create_backend_case("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 ] # create test cases for specific backends sha256_crypt_os_crypt_test = _sha256_crypt_test.create_backend_case("os_crypt") sha256_crypt_builtin_test = _sha256_crypt_test.create_backend_case("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 # create test cases for specific backends sha512_crypt_os_crypt_test = _sha512_crypt_test.create_backend_case("os_crypt") sha512_crypt_builtin_test = _sha512_crypt_test.create_backend_case("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$$.................DUMMY", "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$.................DUMMY", "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 string listed in known_correct_configs (above) # These have to be hash strings, in order to test bare salt issue. if isinstance(hash, str) and hash.endswith("$.................DUMMY"): raise ValueError("pretending '$...' stub 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. 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""" warnings.filterwarnings("ignore", "passing settings to .*.hash\(\) is deprecated") handler = self.handler # preserve hash if provided self.assertEqual(handler.genhash("stub", "!asd"), "!asd") # use marker if no hash self.assertEqual(handler.genhash("stub", ""), handler.default_marker) self.assertEqual(handler.hash("stub"), handler.default_marker) self.assertEqual(handler.using().default_marker, handler.default_marker) # custom marker self.assertEqual(handler.genhash("stub", "", marker="*xxx"), "*xxx") self.assertEqual(handler.hash("stub", marker="*xxx"), "*xxx") self.assertEqual(handler.using(marker="*xxx").hash("stub"), "*xxx") # reject invalid marker self.assertRaises(ValueError, handler.genhash, 'stub', "", marker='abc') self.assertRaises(ValueError, handler.hash, 'stub', marker='abc') self.assertRaises(ValueError, handler.using, marker='abc') class unix_fallback_test(HandlerCase): handler = hash.unix_fallback accepts_all_hashes = 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", ""), "!") self.assertEqual(handler.hash("stub"), "!") # use hash if provided and valid self.assertEqual(handler.genhash("stub", "!asd"), "!asd") #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_handlers_cisco.py0000644000175000017500000004776713043706504023522 0ustar biscuitbiscuit00000000000000""" passlib.tests.test_handlers_cisco - tests for Cisco-specific algorithms """ #============================================================================= # imports #============================================================================= from __future__ import absolute_import, division, print_function # core import logging log = logging.getLogger(__name__) # site # pkg from passlib import hash, exc from passlib.utils.compat import u from .utils import UserHandlerMixin, HandlerCase, repeat_string from .test_handlers import UPASS_TABLE # module __all__ = [ "cisco_pix_test", "cisco_asa_test", "cisco_type7_test", ] #============================================================================= # shared code for cisco PIX & ASA #============================================================================= class _PixAsaSharedTest(UserHandlerMixin, HandlerCase): """ class w/ shared info for PIX & ASA tests. """ __unittest_skip = True # for TestCase requires_user = False # for UserHandlerMixin #: shared list of hashes which should be identical under pix & asa7 #: (i.e. combined secret + user < 17 bytes) pix_asa_shared_hashes = [ # # http://www.perlmonks.org/index.pl?node_id=797623 # (("cisco", ""), "2KFQnbNIdI.2KYOU"), # confirmed ASA 9.6 # # http://www.hsc.fr/ressources/breves/pix_crack.html.en # (("hsc", ""), "YtT8/k6Np8F1yz2c"), # confirmed ASA 9.6 # # www.freerainbowtables.com/phpBB3/viewtopic.php?f=2&t=1441 # (("", ""), "8Ry2YjIyt7RRXU24"), # confirmed ASA 9.6 (("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"), # # http://www.openwall.com/lists/john-users/2010/08/08/3 # (("cangetin", ""), "TynyB./ftknE77QP"), (("cangetin", "rramsey"), "jgBZqYtsWfGcUKDi"), # # from JTR 1.7.9 # ("test1", "TRPEas6f/aa6JSPL"), ("test2", "OMT6mXmAvGyzrCtp"), ("test3", "gTC7RIy1XJzagmLm"), ("test4", "oWC1WRwqlBlbpf/O"), ("password", "NuLKvvWGg.x9HEKO"), ("0123456789abcdef", ".7nfVBEIEu4KbF/1"), # # http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html#wp5472 # (("1234567890123456", ""), "feCkwUGktTCAgIbD"), # canonical source (("watag00s1am", ""), "jMorNbK0514fadBh"), # canonical source # # custom # (("cisco1", "cisco1"), "jmINXNH6p1BxUppp"), # ensures utf-8 used for unicode (UPASS_TABLE, 'CaiIvkLMu2TOHXGT'), # # passlib reference vectors # # Some of these have been confirmed on various ASA firewalls, # and the exact version is noted next to each hash. # Would like to verify these under more PIX & ASA versions. # # Those without a note are generally an extrapolation, # to ensure the code stays consistent, but for various reasons, # hasn't been verified. # # * One such case is usernames w/ 1 & 2 digits -- # ASA (9.6 at least) requires 3+ digits in username. # # The following hashes (below 13 chars) should be identical for PIX/ASA. # Ones which differ are listed separately in the known_correct_hashes # list for the two test classes. # # 4 char password (('1234', ''), 'RLPMUQ26KL4blgFN'), # confirmed ASA 9.6 # 8 char password (('01234567', ''), '0T52THgnYdV1tlOF'), # confirmed ASA 9.6 (('01234567', '3'), '.z0dT9Alkdc7EIGS'), (('01234567', '36'), 'CC3Lam53t/mHhoE7'), (('01234567', '365'), '8xPrWpNnBdD2DzdZ'), # confirmed ASA 9.6 (('01234567', '3333'), '.z0dT9Alkdc7EIGS'), # confirmed ASA 9.6 (('01234567', '3636'), 'CC3Lam53t/mHhoE7'), # confirmed ASA 9.6 (('01234567', '3653'), '8xPrWpNnBdD2DzdZ'), # confirmed ASA 9.6 (('01234567', 'adm'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6 (('01234567', 'adma'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6 (('01234567', 'admad'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6 (('01234567', 'user'), 'PNZ4ycbbZ0jp1.j1'), # confirmed ASA 9.6 (('01234567', 'user1234'), 'PNZ4ycbbZ0jp1.j1'), # confirmed ASA 9.6 # 12 char password (('0123456789ab', ''), 'S31BxZOGlAigndcJ'), # confirmed ASA 9.6 (('0123456789ab', '36'), 'wFqSX91X5.YaRKsi'), (('0123456789ab', '365'), 'qjgo3kNgTVxExbno'), # confirmed ASA 9.6 (('0123456789ab', '3333'), 'mcXPL/vIZcIxLUQs'), # confirmed ASA 9.6 (('0123456789ab', '3636'), 'wFqSX91X5.YaRKsi'), # confirmed ASA 9.6 (('0123456789ab', '3653'), 'qjgo3kNgTVxExbno'), # confirmed ASA 9.6 (('0123456789ab', 'user'), 'f.T4BKdzdNkjxQl7'), # confirmed ASA 9.6 (('0123456789ab', 'user1234'), 'f.T4BKdzdNkjxQl7'), # confirmed ASA 9.6 # NOTE: remaining reference vectors for 13+ char passwords # are split up between cisco_pix & cisco_asa tests. # unicode passwords # ASA supposedly uses utf-8 encoding, but entering non-ascii # chars is error-prone, and while UTF-8 appears to be intended, # observed behaviors include: # * ssh cli stripping non-ascii chars entirely # * ASDM web iface double-encoding utf-8 strings ((u("t\xe1ble").encode("utf-8"), 'user'), 'Og8fB4NyF0m5Ed9c'), ((u("t\xe1ble").encode("utf-8").decode("latin-1").encode("utf-8"), 'user'), 'cMvFC2XVBmK/68yB'), # confirmed ASA 9.6 when typed into ASDM ] def test_calc_digest_spoiler(self): """ _calc_checksum() -- spoil oversize passwords during verify for details, see 'spoil_digest' flag instead that function. this helps cisco_pix/cisco_asa implement their policy of ``.truncate_verify_reject=True``. """ def calc(secret, for_hash=False): return self.handler(use_defaults=for_hash)._calc_checksum(secret) # short (non-truncated) password short_secret = repeat_string("1234", self.handler.truncate_size) short_hash = calc(short_secret) # longer password should have totally different hash, # to prevent verify from matching (i.e. "spoiled"). long_secret = short_secret + "X" long_hash = calc(long_secret) self.assertNotEqual(long_hash, short_hash) # spoiled hash should depend on whole secret, # so that output isn't predictable alt_long_secret = short_secret + "Y" alt_long_hash = calc(alt_long_secret) self.assertNotEqual(alt_long_hash, short_hash) self.assertNotEqual(alt_long_hash, long_hash) # for hash(), should throw error if password too large calc(short_secret, for_hash=True) self.assertRaises(exc.PasswordSizeError, calc, long_secret, for_hash=True) self.assertRaises(exc.PasswordSizeError, calc, alt_long_secret, for_hash=True) #============================================================================= # cisco pix #============================================================================= class cisco_pix_test(_PixAsaSharedTest): handler = hash.cisco_pix #: known correct pix hashes known_correct_hashes = _PixAsaSharedTest.pix_asa_shared_hashes + [ # # passlib reference vectors (PIX-specific) # # NOTE: See 'pix_asa_shared_hashes' for general PIX+ASA vectors, # and general notes about the 'passlib reference vectors' test set. # # All of the following are PIX-specific, as ASA starts # to use a different padding size at 13 characters. # # TODO: these need confirming w/ an actual PIX system. # # 13 char password (('0123456789abc', ''), 'eacOpB7vE7ZDukSF'), (('0123456789abc', '3'), 'ylJTd/qei66WZe3w'), (('0123456789abc', '36'), 'hDx8QRlUhwd6bU8N'), (('0123456789abc', '365'), 'vYOOtnkh1HXcMrM7'), (('0123456789abc', '3333'), 'ylJTd/qei66WZe3w'), (('0123456789abc', '3636'), 'hDx8QRlUhwd6bU8N'), (('0123456789abc', '3653'), 'vYOOtnkh1HXcMrM7'), (('0123456789abc', 'user'), 'f4/.SALxqDo59mfV'), (('0123456789abc', 'user1234'), 'f4/.SALxqDo59mfV'), # 14 char password (('0123456789abcd', ''), '6r8888iMxEoPdLp4'), (('0123456789abcd', '3'), 'f5lvmqWYj9gJqkIH'), (('0123456789abcd', '36'), 'OJJ1Khg5HeAYBH1c'), (('0123456789abcd', '365'), 'OJJ1Khg5HeAYBH1c'), (('0123456789abcd', '3333'), 'f5lvmqWYj9gJqkIH'), (('0123456789abcd', '3636'), 'OJJ1Khg5HeAYBH1c'), (('0123456789abcd', '3653'), 'OJJ1Khg5HeAYBH1c'), (('0123456789abcd', 'adm'), 'DbPLCFIkHc2SiyDk'), (('0123456789abcd', 'adma'), 'DbPLCFIkHc2SiyDk'), (('0123456789abcd', 'user'), 'WfO2UiTapPkF/FSn'), (('0123456789abcd', 'user1234'), 'WfO2UiTapPkF/FSn'), # 15 char password (('0123456789abcde', ''), 'al1e0XFIugTYLai3'), (('0123456789abcde', '3'), 'lYbwBu.f82OIApQB'), (('0123456789abcde', '36'), 'lYbwBu.f82OIApQB'), (('0123456789abcde', '365'), 'lYbwBu.f82OIApQB'), (('0123456789abcde', '3333'), 'lYbwBu.f82OIApQB'), (('0123456789abcde', '3636'), 'lYbwBu.f82OIApQB'), (('0123456789abcde', '3653'), 'lYbwBu.f82OIApQB'), (('0123456789abcde', 'adm'), 'KgKx1UQvdR/09i9u'), (('0123456789abcde', 'adma'), 'KgKx1UQvdR/09i9u'), (('0123456789abcde', 'user'), 'qLopkenJ4WBqxaZN'), (('0123456789abcde', 'user1234'), 'qLopkenJ4WBqxaZN'), # 16 char password (('0123456789abcdef', ''), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', '36'), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', '365'), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', '3333'), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', '3636'), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', '3653'), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', 'user'), '.7nfVBEIEu4KbF/1'), (('0123456789abcdef', 'user1234'), '.7nfVBEIEu4KbF/1'), ] #============================================================================= # cisco asa #============================================================================= class cisco_asa_test(_PixAsaSharedTest): handler = hash.cisco_asa known_correct_hashes = _PixAsaSharedTest.pix_asa_shared_hashes + [ # # passlib reference vectors (ASA-specific) # # NOTE: See 'pix_asa_shared_hashes' for general PIX+ASA vectors, # and general notes about the 'passlib reference vectors' test set. # # 13 char password # NOTE: past this point, ASA pads to 32 bytes instead of 16 # for all cases where user is set (secret + 4 bytes > 16), # but still uses 16 bytes for enable pwds (secret <= 16). # hashes w/ user WON'T match PIX, but "enable" passwords will. (('0123456789abc', ''), 'eacOpB7vE7ZDukSF'), # confirmed ASA 9.6 (('0123456789abc', '36'), 'FRV9JG18UBEgX0.O'), (('0123456789abc', '365'), 'NIwkusG9hmmMy6ZQ'), # confirmed ASA 9.6 (('0123456789abc', '3333'), 'NmrkP98nT7RAeKZz'), # confirmed ASA 9.6 (('0123456789abc', '3636'), 'FRV9JG18UBEgX0.O'), # confirmed ASA 9.6 (('0123456789abc', '3653'), 'NIwkusG9hmmMy6ZQ'), # confirmed ASA 9.6 (('0123456789abc', 'user'), '8Q/FZeam5ai1A47p'), # confirmed ASA 9.6 (('0123456789abc', 'user1234'), '8Q/FZeam5ai1A47p'), # confirmed ASA 9.6 # 14 char password (('0123456789abcd', ''), '6r8888iMxEoPdLp4'), # confirmed ASA 9.6 (('0123456789abcd', '3'), 'yxGoujXKPduTVaYB'), (('0123456789abcd', '36'), 'W0jckhnhjnr/DiT/'), (('0123456789abcd', '365'), 'HuVOxfMQNahaoF8u'), # confirmed ASA 9.6 (('0123456789abcd', '3333'), 'yxGoujXKPduTVaYB'), # confirmed ASA 9.6 (('0123456789abcd', '3636'), 'W0jckhnhjnr/DiT/'), # confirmed ASA 9.6 (('0123456789abcd', '3653'), 'HuVOxfMQNahaoF8u'), # confirmed ASA 9.6 (('0123456789abcd', 'adm'), 'RtOmSeoCs4AUdZqZ'), # confirmed ASA 9.6 (('0123456789abcd', 'adma'), 'RtOmSeoCs4AUdZqZ'), # confirmed ASA 9.6 (('0123456789abcd', 'user'), 'rrucwrcM0h25pr.m'), # confirmed ASA 9.6 (('0123456789abcd', 'user1234'), 'rrucwrcM0h25pr.m'), # confirmed ASA 9.6 # 15 char password (('0123456789abcde', ''), 'al1e0XFIugTYLai3'), # confirmed ASA 9.6 (('0123456789abcde', '3'), 'nAZrQoHaL.fgrIqt'), (('0123456789abcde', '36'), '2GxIQ6ICE795587X'), (('0123456789abcde', '365'), 'QmDsGwCRBbtGEKqM'), # confirmed ASA 9.6 (('0123456789abcde', '3333'), 'nAZrQoHaL.fgrIqt'), # confirmed ASA 9.6 (('0123456789abcde', '3636'), '2GxIQ6ICE795587X'), # confirmed ASA 9.6 (('0123456789abcde', '3653'), 'QmDsGwCRBbtGEKqM'), # confirmed ASA 9.6 (('0123456789abcde', 'adm'), 'Aj2aP0d.nk62wl4m'), # confirmed ASA 9.6 (('0123456789abcde', 'adma'), 'Aj2aP0d.nk62wl4m'), # confirmed ASA 9.6 (('0123456789abcde', 'user'), 'etxiXfo.bINJcXI7'), # confirmed ASA 9.6 (('0123456789abcde', 'user1234'), 'etxiXfo.bINJcXI7'), # confirmed ASA 9.6 # 16 char password (('0123456789abcdef', ''), '.7nfVBEIEu4KbF/1'), # confirmed ASA 9.6 (('0123456789abcdef', '36'), 'GhI8.yFSC5lwoafg'), (('0123456789abcdef', '365'), 'KFBI6cNQauyY6h/G'), # confirmed ASA 9.6 (('0123456789abcdef', '3333'), 'Ghdi1IlsswgYzzMH'), # confirmed ASA 9.6 (('0123456789abcdef', '3636'), 'GhI8.yFSC5lwoafg'), # confirmed ASA 9.6 (('0123456789abcdef', '3653'), 'KFBI6cNQauyY6h/G'), # confirmed ASA 9.6 (('0123456789abcdef', 'user'), 'IneB.wc9sfRzLPoh'), # confirmed ASA 9.6 (('0123456789abcdef', 'user1234'), 'IneB.wc9sfRzLPoh'), # confirmed ASA 9.6 # 17 char password # NOTE: past this point, ASA pads to 32 bytes instead of 16 # for ALL cases, since secret > 16 bytes even for enable pwds; # and so none of these rest here should match PIX. (('0123456789abcdefq', ''), 'bKshl.EN.X3CVFRQ'), # confirmed ASA 9.6 (('0123456789abcdefq', '36'), 'JAeTXHs0n30svlaG'), (('0123456789abcdefq', '365'), '4fKSSUBHT1ChGqHp'), # confirmed ASA 9.6 (('0123456789abcdefq', '3333'), 'USEJbxI6.VY4ecBP'), # confirmed ASA 9.6 (('0123456789abcdefq', '3636'), 'JAeTXHs0n30svlaG'), # confirmed ASA 9.6 (('0123456789abcdefq', '3653'), '4fKSSUBHT1ChGqHp'), # confirmed ASA 9.6 (('0123456789abcdefq', 'user'), '/dwqyD7nGdwSrDwk'), # confirmed ASA 9.6 (('0123456789abcdefq', 'user1234'), '/dwqyD7nGdwSrDwk'), # confirmed ASA 9.6 # 27 char password (('0123456789abcdefqwertyuiopa', ''), '4wp19zS3OCe.2jt5'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopa', '36'), 'PjUoGqWBKPyV9qOe'), (('0123456789abcdefqwertyuiopa', '365'), 'bfCy6xFAe5O/gzvM'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopa', '3333'), 'rd/ZMuGTJFIb2BNG'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopa', '3636'), 'PjUoGqWBKPyV9qOe'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopa', '3653'), 'bfCy6xFAe5O/gzvM'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopa', 'user'), 'zynfWw3UtszxLMgL'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopa', 'user1234'), 'zynfWw3UtszxLMgL'), # confirmed ASA 9.6 # 28 char password # NOTE: past this point, ASA stops appending the username AT ALL, # even though there's still room for the first few chars. (('0123456789abcdefqwertyuiopas', ''), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopas', '36'), 'W6nbOddI0SutTK7m'), (('0123456789abcdefqwertyuiopas', '365'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopas', 'user'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopas', 'user1234'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6 # 32 char password # NOTE: this is max size that ASA allows, and throws error for larger (('0123456789abcdefqwertyuiopasdfgh', ''), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopasdfgh', '36'), '5hPT/iC6DnoBxo6a'), (('0123456789abcdefqwertyuiopasdfgh', '365'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopasdfgh', 'user'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 (('0123456789abcdefqwertyuiopasdfgh', 'user1234'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6 ] #============================================================================= # 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) self.assertRaises(ValueError, handler, salt=100) self.assertRaises(TypeError, handler.using, salt='abc') self.assertRaises(ValueError, handler.using, salt=-10) self.assertRaises(ValueError, handler.using, salt=100) with self.assertWarningList("salt/offset must be.*"): subcls = handler.using(salt=100, relaxed=True) self.assertEqual(subcls(use_defaults=True).salt, 52) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/backports.py0000644000175000017500000000472113015205366021451 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 PY26 # local __all__ = [ "TestCase", "skip", "skipIf", "skipUnless" ] #============================================================================= # import latest unittest module available #============================================================================= try: import unittest2 as unittest except ImportError: if PY26: raise ImportError("Passlib's tests require 'unittest2' under Python 2.6 (as of Passlib 1.7)") # python 2.7 and python 3.2 both have unittest2 features (at least, the ones we use) import unittest #============================================================================= # unittest aliases #============================================================================= skip = unittest.skip skipIf = unittest.skipIf skipUnless = unittest.skipUnless SkipTest = unittest.SkipTest #============================================================================= # custom test harness #============================================================================= class TestCase(unittest.TestCase): """backports a number of unittest2 features in TestCase""" #=================================================================== # backport some unittest2 names #=================================================================== #--------------------------------------------------------------- # backport assertRegex() alias from 3.2 to 2.7 # was present in 2.7 under an alternate name #--------------------------------------------------------------- if not hasattr(unittest.TestCase, "assertRegex"): assertRegex = unittest.TestCase.assertRegexpMatches if not hasattr(unittest.TestCase, "assertRaisesRegex"): assertRaisesRegex = unittest.TestCase.assertRaisesRegexp #=================================================================== # eoc #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_handlers_django.py0000644000175000017500000003540513015205366023645 0ustar biscuitbiscuit00000000000000"""passlib.tests.test_handlers_django - tests for passlib hash algorithms""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import logging; log = logging.getLogger(__name__) import warnings # site # pkg from passlib import hash from passlib.utils import repeat_string from passlib.utils.compat import u from passlib.tests.utils import TestCase, HandlerCase, skipUnless, SkipTest from passlib.tests.test_handlers import UPASS_USD, UPASS_TABLE from passlib.tests.test_ext_django import DJANGO_VERSION, MIN_DJANGO_VERSION # module #============================================================================= # django #============================================================================= # standard string django uses UPASS_LETMEIN = u('l\xe8tmein') def vstr(version): return ".".join(str(e) for e in version) class _DjangoHelper(TestCase): __unittest_skip = True #: minimum django version where hash alg is present / that we support testing against min_django_version = MIN_DJANGO_VERSION #: max django version where hash alg is present max_django_version = None def _require_django_support(self): if DJANGO_VERSION < self.min_django_version: raise self.skipTest("Django >= %s not installed" % vstr(self.min_django_version)) if self.max_django_version and DJANGO_VERSION > self.max_django_version: raise self.skipTest("Django <= %s not installed" % vstr(self.max_django_version)) return True extra_fuzz_verifiers = HandlerCase.fuzz_verifiers + ( "fuzz_verifier_django", ) def fuzz_verifier_django(self): try: self._require_django_support() except SkipTest: return None from django.contrib.auth.hashers import check_password def verify_django(secret, hash): """django/check_password""" if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"): hash = hash.replace("$$2y$", "$$2a$") if 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()""" self._require_django_support() # XXX: esp. when it's no longer supported by django, # should verify it's *NOT* recognized from django.contrib.auth.hashers import check_password assert self.known_correct_hashes for secret, hash in self.iter_known_hashes(): 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()""" self._require_django_support() # XXX: esp. when it's no longer supported by django, # should verify it's *NOT* recognized 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 generator = self.FuzzHashGenerator(self, self.getRandom()) while tick() < end: secret, other = generator.random_password_pair() if not secret: # django rejects empty passwords. continue if self.django_has_encoding_glitch and isinstance(secret, bytes): # e.g. unsalted_md5 tried 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 disabled_contains_salt = 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 max_django_version = (1,9) 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 max_django_version = (1,9) 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', ] class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def random_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 default = handler.default_salt_size assert handler.min_salt_size == 0 lower = 1 upper = handler.max_salt_size or default*4 return self.randintgauss(lower, upper, default, default*.5) class django_salted_sha1_test(HandlerCase, _DjangoHelper): """test django_salted_sha1""" handler = hash.django_salted_sha1 max_django_version = (1,9) 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', ] # reuse custom random_salt_size() helper... FuzzHashGenerator = django_salted_md5_test.FuzzHashGenerator class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper): """test django_pbkdf2_sha256""" handler = hash.django_pbkdf2_sha256 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 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='), ] @skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available") class django_bcrypt_test(HandlerCase, _DjangoHelper): """test django_bcrypt""" handler = hash.django_bcrypt 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) class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.randintgauss(5, 8, 6, 1) def random_ident(self): # omit multi-ident tests, only $2a$ counts for this class # XXX: enable this to check 2a / 2b? return None @skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available") class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper): """test django_bcrypt_sha256""" handler = hash.django_bcrypt_sha256 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', ] # 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) class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.randintgauss(5, 8, 6, 1) def random_ident(self): # omit multi-ident tests, only $2a$ counts for this class # XXX: enable this to check 2a / 2b? return None from passlib.tests.test_handlers_argon2 import _base_argon2_test @skipUnless(hash.argon2.has_backend(), "no argon2 backends available") class django_argon2_test(HandlerCase, _DjangoHelper): """test django_bcrypt""" handler = hash.django_argon2 # NOTE: most of this adapted from _base_argon2_test & argon2pure test known_correct_hashes = [ # sample test ("password", 'argon2$argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A'), # sample w/ all parameters different ("password", 'argon2$argon2i$v=19$m=380,t=2,p=2$c29tZXNhbHQ$SrssP8n7m/12VWPM8dvNrw'), # generated from django 1.10.3 (UPASS_LETMEIN, 'argon2$argon2i$v=19$m=512,t=2,p=2$V25jN1l4UUJZWkR1$MxpA1BD2Gh7+D79gaAw6sQ'), ] def setUpWarnings(self): super(django_argon2_test, self).setUpWarnings() warnings.filterwarnings("ignore", ".*Using argon2pure backend.*") def do_stub_encrypt(self, handler=None, **settings): # overriding default since no way to get stub config from argon2._calc_hash() # (otherwise test_21b_max_rounds blocks trying to do max rounds) handler = (handler or self.handler).using(**settings) self = handler.wrapped(use_defaults=True) self.checksum = self._stub_checksum assert self.checksum return handler._wrap_hash(self.to_string()) def test_03_legacy_hash_workflow(self): # override base method raise self.skipTest("legacy 1.6 workflow not supported") class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator): def random_rounds(self): # decrease default rounds for fuzz testing to speed up volume. return self.randintgauss(1, 3, 2, 1) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/utils.py0000644000175000017500000041612613043701620020622 0ustar biscuitbiscuit00000000000000"""helpers for passlib unittests""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from binascii import unhexlify import contextlib from functools import wraps, partial import hashlib import logging; log = logging.getLogger(__name__) import random import re import os import sys import tempfile import threading import time from passlib.exc import PasslibHashWarning, PasslibConfigWarning from passlib.utils.compat import PY3, JYTHON import warnings from warnings import warn # site # pkg from passlib import exc from passlib.exc import MissingBackendError import passlib.registry as registry from passlib.tests.backports import TestCase as _TestCase, skip, skipIf, skipUnless, SkipTest from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \ rng as sys_rng, getrandstr, is_ascii_safe, to_native_str, \ repeat_string, tick, batch from passlib.utils.compat import iteritems, irange, u, unicode, PY2 from passlib.utils.decor import classproperty 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_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 get_effective_rounds(handler, rounds=None): """get effective rounds value from handler""" handler = unwrap_handler(handler) return handler(rounds=rounds, use_defaults=True).rounds def is_default_backend(handler, backend): """check if backend is the default for source""" try: orig = handler.get_backend() except MissingBackendError: return False try: handler.set_backend("default") return handler.get_backend() == backend finally: handler.set_backend(orig) def iter_alt_backends(handler, current=None, fallback=False): """ iterate over alternate backends available to handler. .. warning:: not thread-safe due to has_backend() call """ if current is None: current = handler.get_backend() backends = handler.backends idx = backends.index(current)+1 if fallback else 0 for backend in backends[idx:]: if backend != current and handler.has_backend(backend): yield backend def get_alt_backend(*args, **kwds): for backend in iter_alt_backends(*args, **kwds): return backend return None def unwrap_handler(handler): """return original handler, removing any wrapper objects""" while hasattr(handler, "wrapped"): handler = handler.wrapped return handler def handler_derived_from(handler, base): """ test if was derived from via . """ # XXX: need way to do this more formally via ifc, # for now just hacking in the cases we encounter in testing. if handler == base: return True elif isinstance(handler, uh.PrefixWrapper): while handler: if handler == base: return True # helper set by PrefixWrapper().using() just for this case... handler = handler._derived_from return False elif isinstance(handler, type) and issubclass(handler, uh.MinimalHandler): return issubclass(handler, base) else: raise NotImplementedError("don't know how to inspect handler: %r" % (handler,)) @contextlib.contextmanager def patch_calc_min_rounds(handler): """ internal helper for do_config_encrypt() -- context manager which temporarily replaces handler's _calc_checksum() with one that uses min_rounds; useful when trying to generate config with high rounds value, but don't care if output is correct. """ if isinstance(handler, type) and issubclass(handler, uh.HasRounds): # XXX: also require GenericHandler for this branch? wrapped = handler._calc_checksum def wrapper(self, *args, **kwds): rounds = self.rounds try: self.rounds = self.min_rounds return wrapped(self, *args, **kwds) finally: self.rounds = rounds handler._calc_checksum = wrapper try: yield finally: handler._calc_checksum = wrapped elif isinstance(handler, uh.PrefixWrapper): with patch_calc_min_rounds(handler.wrapped): yield else: yield return #============================================================================= # 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 hb(source): """ helper for represent byte strings in hex. usage: ``hb("deadbeef23")`` """ return unhexlify(re.sub(r"\s", "", source)) def limit(value, lower, upper): if value < lower: return lower elif value > upper: return upper return value def quicksleep(delay): """because time.sleep() doesn't even have 10ms accuracy on some OSes""" start = tick() while tick()-start < delay: pass def time_call(func, setup=None, maxtime=1, bestof=10): """ timeit() wrapper which tries to get as accurate a measurement as possible w/in maxtime seconds. :returns: ``(avg_seconds_per_call, log10_number_of_repetitions)`` """ from timeit import Timer from math import log timer = Timer(func, setup=setup or '') number = 1 end = tick() + maxtime while True: delta = min(timer.repeat(bestof, number)) if tick() >= end: return delta/number, int(log(number, 10)) number *= 10 def run_with_fixed_seeds(count=128, master_seed=0x243F6A8885A308D3): """ decorator run test method w/ multiple fixed seeds. """ def builder(func): @wraps(func) def wrapper(*args, **kwds): rng = random.Random(master_seed) for _ in irange(count): kwds['seed'] = rng.getrandbits(32) func(*args, **kwds) return wrapper return builder #============================================================================= # custom test harness #============================================================================= 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) @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__) # ignore warnings about PasswordHash features deprecated in 1.7 # TODO: should be cleaned in 2.0, when support will be dropped. # should be kept until then, so we test the legacy paths. warnings.filterwarnings("ignore", r"the method .*\.(encrypt|genconfig|genhash)\(\) is deprecated") warnings.filterwarnings("ignore", r"the 'vary_rounds' option is deprecated") #--------------------------------------------------------------- # 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, # and to return the caught exception for further examination #--------------------------------------------------------------- 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 as err: return err 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(warnings.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 exc_info[0] is None: 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") #=================================================================== # reproducible random helpers #=================================================================== #: global thread lock for random state #: XXX: could split into global & per-instance locks if need be _random_global_lock = threading.Lock() #: cache of global seed value, initialized on first call to getRandom() _random_global_seed = None #: per-instance cache of name -> RNG _random_cache = None def getRandom(self, name="default", seed=None): """ Return a :class:`random.Random` object for current test method to use. Within an instance, multiple calls with the same name will return the same object. When first created, each RNG will be seeded with value derived from a global seed, the test class module & name, the current test method name, and the **name** parameter. The global seed taken from the $RANDOM_TEST_SEED env var, the $PYTHONHASHSEED env var, or a randomly generated the first time this method is called. In all cases, the value is logged for reproducibility. :param name: name to uniquely identify separate RNGs w/in a test (e.g. for threaded tests). :param seed: override global seed when initialzing rng. :rtype: random.Random """ # check cache cache = self._random_cache if cache and name in cache: return cache[name] with self._random_global_lock: # check cache again, and initialize it cache = self._random_cache if cache and name in cache: return cache[name] elif not cache: cache = self._random_cache = {} # init global seed global_seed = seed or TestCase._random_global_seed if global_seed is None: # NOTE: checking PYTHONHASHSEED, because if that's set, # the test runner wants something reproducible. global_seed = TestCase._random_global_seed = \ int(os.environ.get("RANDOM_TEST_SEED") or os.environ.get("PYTHONHASHSEED") or sys_rng.getrandbits(32)) # XXX: would it be better to print() this? log.info("using RANDOM_TEST_SEED=%d", global_seed) # create seed cls = type(self) source = "\n".join([str(global_seed), cls.__module__, cls.__name__, self._testMethodName, name]) digest = hashlib.sha256(source.encode("utf-8")).hexdigest() seed = int(digest[:16], 16) # create rng value = cache[name] = random.Random(seed) return value #=================================================================== # 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 def patchAttr(self, obj, attr, value, require_existing=True, wrap=False): """monkeypatch object value, restoring original value on cleanup""" try: orig = getattr(obj, attr) except AttributeError: if require_existing: raise def cleanup(): try: delattr(obj, attr) except AttributeError: pass self.addCleanup(cleanup) else: self.addCleanup(setattr, obj, attr, orig) if wrap: value = partial(value, orig) wraps(orig)(value) setattr(obj, attr, value) #=================================================================== # 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 hash behavior - generally # don't need to be overidden. stock_passwords = [ u("test"), u("\u20AC\u00A5$"), b'\xe2\x82\xac\xc2\xa5$' ] #--------------------------------------------------------------- # option flags #--------------------------------------------------------------- # 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 if scheme has "is_disabled" set, and contains 'salted' data disabled_contains_salt = 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 #=================================================================== # support methods #=================================================================== #--------------------------------------------------------------- # configuration helpers #--------------------------------------------------------------- @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 self.getRandom().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.handler.is_disabled 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 # NOTE: skipping warning if default salt size is already maxed out # (might change that in future) if 'salt_size' not in cls.setting_kwds and (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") for c in cls.default_salt_chars: if c not in cls.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 hash() / 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/10**d, just # to eliminate false-positives. which works out to n>3.33+d-salt_bits. # for 1/1e12 odds, n=1 is sufficient for most hashes, but a few border cases (e.g. # cisco_type7) have < 16 bits of salt, requiring more. samples = max(1, 4 + 12 - self.salt_bits) def sampler(func): value1 = func() for _ in irange(samples): value2 = func() if value1 != value2: return raise self.failureException("failed to find different salt after " "%d samples" % (samples,)) sampler(self.do_genconfig) sampler(lambda: self.do_encrypt("stub")) def test_12_min_salt_size(self): """test hash() / 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 hash() / 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] # NOTE: skipping this for hashes like argon2 since max_salt_size takes WAY too much memory if max_size is None or max_size > (1 << 20): # # 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_stub_encrypt(salt=s1) c2 = self.do_stub_encrypt(salt=s1 + salt_char) self.assertNotEqual(c1, c2) self.do_stub_encrypt(salt_size=1024) else: # # check max size is accepted # s1 = salt_char * max_size c1 = self.do_stub_encrypt(salt=s1) self.do_stub_encrypt(salt_size=max_size) # # check max size + 1 is rejected # s2 = s1 + salt_char self.assertRaises(ValueError, self.do_stub_encrypt, salt=s2) self.assertRaises(ValueError, self.do_stub_encrypt, salt_size=max_size + 1) # # should accept too-large salt in relaxed mode # if has_relaxed_setting(handler): with warnings.catch_warnings(record=True): # issues passlibhandlerwarning c2 = self.do_stub_encrypt(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_stub_encrypt(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.binary import bcrypt64 salt = bcrypt64.repair_unused(salt) return salt def test_14_salt_chars(self): """test hash() 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 for salt in batch(cs, mx or 32): if len(salt) < mn: salt = repeat_string(salt, mn) salt = self.prepare_salt(salt) self.do_stub_encrypt(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_stub_encrypt, 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 salt_size = getattr(self.handler, "min_salt_size", 0) or 8 # 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') * salt_size) # 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' * salt_size) def test_using_salt_size(self): """Handler.using() -- default_salt_size""" self.require_salt_info() handler = self.handler mn = handler.min_salt_size mx = handler.max_salt_size df = handler.default_salt_size # should prevent setting below handler limit self.assertRaises(ValueError, handler.using, default_salt_size=-1) with self.assertWarningList([PasslibHashWarning]): temp = handler.using(default_salt_size=-1, relaxed=True) self.assertEqual(temp.default_salt_size, mn) # should prevent setting above handler limit if mx: self.assertRaises(ValueError, handler.using, default_salt_size=mx+1) with self.assertWarningList([PasslibHashWarning]): temp = handler.using(default_salt_size=mx+1, relaxed=True) self.assertEqual(temp.default_salt_size, mx) # try setting to explicit value if mn != mx: temp = handler.using(default_salt_size=mn+1) self.assertEqual(temp.default_salt_size, mn+1) self.assertEqual(handler.default_salt_size, df) temp = handler.using(default_salt_size=mn+2) self.assertEqual(temp.default_salt_size, mn+2) self.assertEqual(handler.default_salt_size, df) # accept strings if mn == mx: ref = mn else: ref = mn + 1 temp = handler.using(default_salt_size=str(ref)) self.assertEqual(temp.default_salt_size, ref) # reject invalid strings self.assertRaises(ValueError, handler.using, default_salt_size=str(ref) + "xxx") # honor 'salt_size' alias temp = handler.using(salt_size=ref) self.assertEqual(temp.default_salt_size, ref) #=================================================================== # 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_min_rounds(self): """test hash() / genconfig() honors min_rounds""" 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 def test_21b_max_rounds(self): """test hash() / genconfig() honors max_rounds""" self.require_rounds_info() handler = self.handler max_rounds = handler.max_rounds if max_rounds is not None: # 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) # handle max rounds if max_rounds is None: self.do_stub_encrypt(rounds=(1 << 31) - 1) else: self.do_stub_encrypt(rounds=max_rounds) # TODO: check relaxed mode clips max+1 #-------------------------------------------------------------------------------------- # HasRounds.using() / .needs_update() -- desired rounds limits #-------------------------------------------------------------------------------------- def _create_using_rounds_helper(self): """ setup test helpers for testing handler.using()'s rounds parameters. """ self.require_rounds_info() handler = self.handler if handler.name == "bsdi_crypt": # hack to bypass bsdi-crypt's "odd rounds only" behavior, messes up this test orig_handler = handler handler = handler.using() handler._generate_rounds = classmethod(lambda cls: super(orig_handler, cls)._generate_rounds()) # create some fake values to test with orig_min_rounds = handler.min_rounds orig_max_rounds = handler.max_rounds orig_default_rounds = handler.default_rounds medium = ((orig_max_rounds or 9999) + orig_min_rounds) // 2 if medium == orig_default_rounds: medium += 1 small = (orig_min_rounds + medium) // 2 large = ((orig_max_rounds or 9999) + medium) // 2 if handler.name == "bsdi_crypt": # hack to avoid even numbered rounds small |= 1 medium |= 1 large |= 1 adj = 2 else: adj = 1 # create a subclass with small/medium/large as new default desired values with self.assertWarningList([]): subcls = handler.using( min_desired_rounds=small, max_desired_rounds=large, default_rounds=medium, ) # return helpers return handler, subcls, small, medium, large, adj def test_has_rounds_using_harness(self): """ HasRounds.using() -- sanity check test harness """ # setup helpers self.require_rounds_info() handler = self.handler orig_min_rounds = handler.min_rounds orig_max_rounds = handler.max_rounds orig_default_rounds = handler.default_rounds handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() # shouldn't affect original handler at all self.assertEqual(handler.min_rounds, orig_min_rounds) self.assertEqual(handler.max_rounds, orig_max_rounds) self.assertEqual(handler.min_desired_rounds, None) self.assertEqual(handler.max_desired_rounds, None) self.assertEqual(handler.default_rounds, orig_default_rounds) # should affect subcls' desired value, but not hard min/max self.assertEqual(subcls.min_rounds, orig_min_rounds) self.assertEqual(subcls.max_rounds, orig_max_rounds) self.assertEqual(subcls.default_rounds, medium) self.assertEqual(subcls.min_desired_rounds, small) self.assertEqual(subcls.max_desired_rounds, large) def test_has_rounds_using_w_min_rounds(self): """ HasRounds.using() -- min_rounds / min_desired_rounds """ # setup helpers handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() orig_min_rounds = handler.min_rounds orig_max_rounds = handler.max_rounds orig_default_rounds = handler.default_rounds # .using() should clip values below valid minimum, w/ warning if orig_min_rounds > 0: self.assertRaises(ValueError, handler.using, min_desired_rounds=orig_min_rounds - adj) with self.assertWarningList([PasslibHashWarning]): temp = handler.using(min_desired_rounds=orig_min_rounds - adj, relaxed=True) self.assertEqual(temp.min_desired_rounds, orig_min_rounds) # .using() should clip values above valid maximum, w/ warning if orig_max_rounds: self.assertRaises(ValueError, handler.using, min_desired_rounds=orig_max_rounds + adj) with self.assertWarningList([PasslibHashWarning]): temp = handler.using(min_desired_rounds=orig_max_rounds + adj, relaxed=True) self.assertEqual(temp.min_desired_rounds, orig_max_rounds) # .using() should allow values below previous desired minimum, w/o warning with self.assertWarningList([]): temp = subcls.using(min_desired_rounds=small - adj) self.assertEqual(temp.min_desired_rounds, small - adj) # .using() should allow values w/in previous range temp = subcls.using(min_desired_rounds=small + 2 * adj) self.assertEqual(temp.min_desired_rounds, small + 2 * adj) # .using() should allow values above previous desired maximum, w/o warning with self.assertWarningList([]): temp = subcls.using(min_desired_rounds=large + adj) self.assertEqual(temp.min_desired_rounds, large + adj) # hash() etc should allow explicit values below desired minimum # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using() self.assertEqual(get_effective_rounds(subcls, small + adj), small + adj) self.assertEqual(get_effective_rounds(subcls, small), small) with self.assertWarningList([]): self.assertEqual(get_effective_rounds(subcls, small - adj), small - adj) # 'min_rounds' should be treated as alias for 'min_desired_rounds' temp = handler.using(min_rounds=small) self.assertEqual(temp.min_desired_rounds, small) # should be able to specify strings temp = handler.using(min_rounds=str(small)) self.assertEqual(temp.min_desired_rounds, small) # invalid strings should cause error self.assertRaises(ValueError, handler.using, min_rounds=str(small) + "xxx") def test_has_rounds_replace_w_max_rounds(self): """ HasRounds.using() -- max_rounds / max_desired_rounds """ # setup helpers handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() orig_min_rounds = handler.min_rounds orig_max_rounds = handler.max_rounds # .using() should clip values below valid minimum w/ warning if orig_min_rounds > 0: self.assertRaises(ValueError, handler.using, max_desired_rounds=orig_min_rounds - adj) with self.assertWarningList([PasslibHashWarning]): temp = handler.using(max_desired_rounds=orig_min_rounds - adj, relaxed=True) self.assertEqual(temp.max_desired_rounds, orig_min_rounds) # .using() should clip values above valid maximum, w/ warning if orig_max_rounds: self.assertRaises(ValueError, handler.using, max_desired_rounds=orig_max_rounds + adj) with self.assertWarningList([PasslibHashWarning]): temp = handler.using(max_desired_rounds=orig_max_rounds + adj, relaxed=True) self.assertEqual(temp.max_desired_rounds, orig_max_rounds) # .using() should clip values below previous minimum, w/ warning with self.assertWarningList([PasslibConfigWarning]): temp = subcls.using(max_desired_rounds=small - adj) self.assertEqual(temp.max_desired_rounds, small) # .using() should reject explicit min > max self.assertRaises(ValueError, subcls.using, min_desired_rounds=medium+adj, max_desired_rounds=medium-adj) # .using() should allow values w/in previous range temp = subcls.using(min_desired_rounds=large - 2 * adj) self.assertEqual(temp.min_desired_rounds, large - 2 * adj) # .using() should allow values above previous desired maximum, w/o warning with self.assertWarningList([]): temp = subcls.using(max_desired_rounds=large + adj) self.assertEqual(temp.max_desired_rounds, large + adj) # hash() etc should allow explicit values above desired minimum, w/o warning # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using() self.assertEqual(get_effective_rounds(subcls, large - adj), large - adj) self.assertEqual(get_effective_rounds(subcls, large), large) with self.assertWarningList([]): self.assertEqual(get_effective_rounds(subcls, large + adj), large + adj) # 'max_rounds' should be treated as alias for 'max_desired_rounds' temp = handler.using(max_rounds=large) self.assertEqual(temp.max_desired_rounds, large) # should be able to specify strings temp = handler.using(max_desired_rounds=str(large)) self.assertEqual(temp.max_desired_rounds, large) # invalid strings should cause error self.assertRaises(ValueError, handler.using, max_desired_rounds=str(large) + "xxx") def test_has_rounds_using_w_default_rounds(self): """ HasRounds.using() -- default_rounds """ # setup helpers handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() orig_max_rounds = handler.max_rounds # XXX: are there any other cases that need testing? # implicit default rounds -- increase to min_rounds temp = subcls.using(min_rounds=medium+adj) self.assertEqual(temp.default_rounds, medium+adj) # implicit default rounds -- decrease to max_rounds temp = subcls.using(max_rounds=medium-adj) self.assertEqual(temp.default_rounds, medium-adj) # explicit default rounds below desired minimum # XXX: make this a warning if min is implicit? self.assertRaises(ValueError, subcls.using, default_rounds=small-adj) # explicit default rounds above desired maximum # XXX: make this a warning if max is implicit? if orig_max_rounds: self.assertRaises(ValueError, subcls.using, default_rounds=large+adj) # hash() etc should implicit default rounds, but get overridden self.assertEqual(get_effective_rounds(subcls), medium) self.assertEqual(get_effective_rounds(subcls, medium+adj), medium+adj) # should be able to specify strings temp = handler.using(default_rounds=str(medium)) self.assertEqual(temp.default_rounds, medium) # invalid strings should cause error self.assertRaises(ValueError, handler.using, default_rounds=str(medium) + "xxx") def test_has_rounds_using_w_rounds(self): """ HasRounds.using() -- rounds """ # setup helpers handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() orig_max_rounds = handler.max_rounds # 'rounds' should be treated as fallback for min, max, and default temp = subcls.using(rounds=medium+adj) self.assertEqual(temp.min_desired_rounds, medium+adj) self.assertEqual(temp.default_rounds, medium+adj) self.assertEqual(temp.max_desired_rounds, medium+adj) # 'rounds' should be treated as fallback for min, max, and default temp = subcls.using(rounds=medium+1, min_rounds=small+adj, default_rounds=medium, max_rounds=large-adj) self.assertEqual(temp.min_desired_rounds, small+adj) self.assertEqual(temp.default_rounds, medium) self.assertEqual(temp.max_desired_rounds, large-adj) def test_has_rounds_using_w_vary_rounds_parsing(self): """ HasRounds.using() -- vary_rounds parsing """ # setup helpers handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() def parse(value): return subcls.using(vary_rounds=value).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) # float bounds should be enforced self.assertRaises(ValueError, parse, -0.1) self.assertRaises(ValueError, parse, 1.1) def test_has_rounds_using_w_vary_rounds_generation(self): """ HasRounds.using() -- vary_rounds generation """ handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() def get_effective_range(cls): seen = set(get_effective_rounds(cls) for _ in irange(1000)) return min(seen), max(seen) def assert_rounds_range(vary_rounds, lower, upper): temp = subcls.using(vary_rounds=vary_rounds) seen_lower, seen_upper = get_effective_range(temp) self.assertEqual(seen_lower, lower, "vary_rounds had wrong lower limit:") self.assertEqual(seen_upper, upper, "vary_rounds had wrong upper limit:") # test static assert_rounds_range(0, medium, medium) assert_rounds_range("0%", medium, medium) # test absolute assert_rounds_range(adj, medium - adj, medium + adj) assert_rounds_range(50, max(small, medium - 50), min(large, medium + 50)) # test relative - should shift over at 50% mark if handler.rounds_cost == "log2": # log rounds "50%" variance should only increase/decrease by 1 cost value assert_rounds_range("1%", medium, medium) assert_rounds_range("49%", medium, medium) assert_rounds_range("50%", medium - adj, medium) else: # for linear rounds, range is frequently so huge, won't ever see ends. # so we just check it's within an expected range. lower, upper = get_effective_range(subcls.using(vary_rounds="50%")) self.assertGreaterEqual(lower, max(small, medium * 0.5)) self.assertLessEqual(lower, max(small, medium * 0.8)) self.assertGreaterEqual(upper, min(large, medium * 1.2)) self.assertLessEqual(upper, min(large, medium * 1.5)) def test_has_rounds_using_and_needs_update(self): """ HasRounds.using() -- desired_rounds + needs_update() """ handler, subcls, small, medium, large, adj = self._create_using_rounds_helper() temp = subcls.using(min_desired_rounds=small+2, max_desired_rounds=large-2) # generate some sample hashes small_hash = self.do_stub_encrypt(subcls, rounds=small) medium_hash = self.do_stub_encrypt(subcls, rounds=medium) large_hash = self.do_stub_encrypt(subcls, rounds=large) # everything should be w/in bounds for original handler self.assertFalse(subcls.needs_update(small_hash)) self.assertFalse(subcls.needs_update(medium_hash)) self.assertFalse(subcls.needs_update(large_hash)) # small & large should require update for temp handler self.assertTrue(temp.needs_update(small_hash)) self.assertFalse(temp.needs_update(medium_hash)) self.assertTrue(temp.needs_update(large_hash)) #=================================================================== # idents #=================================================================== def require_many_idents(self): handler = self.handler if not isinstance(handler, type) or not issubclass(handler, uh.HasManyIdents): raise self.skipTest("handler doesn't derive from HasManyIdents") def test_30_HasManyIdents(self): """validate HasManyIdents configuration""" cls = self.handler self.require_many_idents() # 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 def test_has_many_idents_using(self): """HasManyIdents.using() -- 'default_ident' and 'ident' keywords""" self.require_many_idents() # pick alt ident to test with handler = self.handler orig_ident = handler.default_ident for alt_ident in handler.ident_values: if alt_ident != orig_ident: break else: raise AssertionError("expected to find alternate ident: default=%r values=%r" % (orig_ident, handler.ident_values)) def effective_ident(cls): cls = unwrap_handler(cls) return cls(use_defaults=True).ident # keep default if nothing else specified subcls = handler.using() self.assertEqual(subcls.default_ident, orig_ident) # accepts alt ident subcls = handler.using(default_ident=alt_ident) self.assertEqual(subcls.default_ident, alt_ident) self.assertEqual(handler.default_ident, orig_ident) # check subcls actually *generates* default ident, # and that we didn't affect orig handler self.assertEqual(effective_ident(subcls), alt_ident) self.assertEqual(effective_ident(handler), orig_ident) # rejects bad ident self.assertRaises(ValueError, handler.using, default_ident='xXx') # honor 'ident' alias subcls = handler.using(ident=alt_ident) self.assertEqual(subcls.default_ident, alt_ident) self.assertEqual(handler.default_ident, orig_ident) # forbid both at same time self.assertRaises(TypeError, handler.using, default_ident=alt_ident, ident=alt_ident) # check ident aliases are being honored if handler.ident_aliases: for alias, ident in handler.ident_aliases.items(): subcls = handler.using(ident=alias) self.assertEqual(subcls.default_ident, ident, msg="alias %r:" % alias) #=================================================================== # password size limits #=================================================================== def test_truncate_error_setting(self): """ validate 'truncate_error' setting & related attributes """ # If it doesn't have truncate_size set, # it shouldn't support truncate_error hasher = self.handler if hasher.truncate_size is None: self.assertNotIn("truncate_error", hasher.setting_kwds) return # if hasher defaults to silently truncating, # it MUST NOT use .truncate_verify_reject, # because resulting hashes wouldn't verify! if not hasher.truncate_error: self.assertFalse(hasher.truncate_verify_reject) # if hasher doesn't have configurable policy, # it must throw error by default if "truncate_error" not in hasher.setting_kwds: self.assertTrue(hasher.truncate_error) return # test value parsing def parse_value(value): return hasher.using(truncate_error=value).truncate_error self.assertEqual(parse_value(None), hasher.truncate_error) self.assertEqual(parse_value(True), True) self.assertEqual(parse_value("true"), True) self.assertEqual(parse_value(False), False) self.assertEqual(parse_value("false"), False) self.assertRaises(ValueError, parse_value, "xxx") def test_secret_wo_truncate_size(self): """ test no password size limits enforced (if truncate_size=None) """ # skip if hasher has a maximum password size hasher = self.handler if hasher.truncate_size is not None: self.assertGreaterEqual(hasher.truncate_size, 1) raise self.skipTest("truncate_size is set") # 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. # hash a 1024-byte secret secret = "too many secrets" * 16 alt = "x" hash = self.do_encrypt(secret) # check that verify doesn't silently reject secret # (i.e. hasher mistakenly honors .truncate_verify_reject) verify_success = not hasher.is_disabled self.assertEqual(self.do_verify(secret, hash), verify_success, msg="verify rejected correct secret") # alter last byte, should get different hash, which won't verify alt_secret = secret[:-1] + alt self.assertFalse(self.do_verify(alt_secret, hash), "full password not used in digest") def test_secret_w_truncate_size(self): """ test password size limits raise truncate_error (if appropriate) """ #-------------------------------------------------- # check if test is applicable #-------------------------------------------------- handler = self.handler truncate_size = handler.truncate_size if not truncate_size: raise self.skipTest("truncate_size not set") #-------------------------------------------------- # setup vars #-------------------------------------------------- # try to get versions w/ and w/o truncate_error set. # set to None if policy isn't configruable size_error_type = exc.PasswordSizeError if "truncate_error" in handler.setting_kwds: without_error = handler.using(truncate_error=False) with_error = handler.using(truncate_error=True) size_error_type = exc.PasswordTruncateError elif handler.truncate_error: without_error = None with_error = handler else: # NOTE: this mode is currently an error in test_truncate_error_setting() without_error = handler with_error = None # create some test secrets base = "too many secrets" alt = "x" # char that's not in base, used to mutate test secrets long_secret = repeat_string(base, truncate_size+1) short_secret = long_secret[:-1] alt_long_secret = long_secret[:-1] + alt alt_short_secret = short_secret[:-1] + alt # init flags short_verify_success = not handler.is_disabled long_verify_success = short_verify_success and \ not handler.truncate_verify_reject #-------------------------------------------------- # do tests on length secret, and resulting hash. # should pass regardless of truncate_error policy. #-------------------------------------------------- assert without_error or with_error for cand_hasher in [without_error, with_error]: # create & hash string that's exactly chars. short_hash = self.do_encrypt(short_secret, handler=cand_hasher) # check hash verifies, regardless of .truncate_verify_reject self.assertEqual(self.do_verify(short_secret, short_hash, handler=cand_hasher), short_verify_success) # changing 'th char should invalidate hash # if this fails, means (reported) truncate_size is too large. self.assertFalse(self.do_verify(alt_short_secret, short_hash, handler=with_error), "truncate_size value is too large") # verify should truncate long secret before comparing # (unless truncate_verify_reject is set) self.assertEqual(self.do_verify(long_secret, short_hash, handler=cand_hasher), long_verify_success) #-------------------------------------------------- # do tests on length secret, # w/ truncate error disabled (should silently truncate) #-------------------------------------------------- if without_error: # create & hash string that's exactly truncate_size+1 chars long_hash = self.do_encrypt(long_secret, handler=without_error) # check verifies against secret (unless truncate_verify_reject=True) self.assertEqual(self.do_verify(long_secret, long_hash, handler=without_error), short_verify_success) # check mutating last char doesn't change outcome. # if this fails, means (reported) truncate_size is too small. self.assertEqual(self.do_verify(alt_long_secret, long_hash, handler=without_error), short_verify_success) # check short_secret verifies against this hash # if this fails, means (reported) truncate_size is too large. self.assertTrue(self.do_verify(short_secret, long_hash, handler=without_error)) #-------------------------------------------------- # do tests on length secret, # w/ truncate error #-------------------------------------------------- if with_error: # with errors enabled, should forbid truncation. err = self.assertRaises(size_error_type, self.do_encrypt, long_secret, handler=with_error) self.assertEqual(err.max_size, truncate_size) #=================================================================== # password contents #=================================================================== 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"] # test hashing lower-case verifies against lower & upper lower = 'test' upper = 'TEST' h1 = self.do_encrypt(lower) if verify_insensitive and not self.handler.is_disabled: 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") # test hashing upper-case verifies against lower & upper h2 = self.do_encrypt(upper) if verify_insensitive and not self.handler.is_disabled: self.assertTrue(self.do_verify(lower, h2), "verify() should not be case sensitive") else: self.assertFalse(self.do_verify(lower, h2), "verify() should be case sensitive") # test genhash # XXX: 2.0: what about 'verify-only' hashes once genhash() is removed? # won't have easy way to recreate w/ same config to see if hash differs. # (though only hash this applies to is mssql2000) h2 = self.do_genhash(upper, h1) if hash_insensitive or (self.handler.is_disabled and not self.disabled_contains_salt): 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) # xxx: move to password size limits section, above? 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] err = self.assertRaises(PasswordSizeError, self.do_genhash, secret, hash) self.assertEqual(err.max_size, MAX_PASSWORD_SIZE) 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,)) if self.handler.is_disabled and self.disabled_contains_salt: continue 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,)) if self.handler.is_disabled and self.disabled_contains_salt: continue 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 handled correctly # self.assertRaises(TypeError, self.do_identify, None) self.assertRaises(TypeError, self.do_verify, 'stub', None) # NOTE: changed in 1.7 -- previously 'None' would be accepted when config strings not supported. self.assertRaises(TypeError, self.do_genhash, 'stub', None) # # 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, threaded=False): """fuzz testing -- 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() :param report_thread_state: if true, writes state of loop to current_thread().passlib_fuzz_state. used to help debug multi-threaded fuzz test issues (below) """ if self.handler.is_disabled: raise self.skipTest("not applicable") # gather info from passlib.utils import tick max_time = self.max_fuzz_time if max_time <= 0: raise self.skipTest("disabled by test mode") verifiers = self.get_fuzz_verifiers(threaded=threaded) def vname(v): return (v.__doc__ or v.__name__).splitlines()[0] # init rng -- using separate one for each thread # so things are predictable for given RANDOM_TEST_SEED # (relies on test_78_fuzz_threading() to give threads unique names) if threaded: thread_name = threading.current_thread().name else: thread_name = "fuzz test" rng = self.getRandom(name=thread_name) generator = self.FuzzHashGenerator(self, rng) # do as many tests as possible for max_time seconds log.debug("%s: %s: started; max_time=%r verifiers=%d (%s)", self.descriptionPrefix, thread_name, max_time, len(verifiers), ", ".join(vname(v) for v in verifiers)) start = tick() stop = start + max_time count = 0 while tick() <= stop: # generate random password & options opts = generator.generate() secret = opts['secret'] other = opts['other'] settings = opts['settings'] ctx = opts['context'] if ctx: settings['context'] = ctx # create new hash hash = self.do_encrypt(secret, **settings) ##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 %r verifier: " "secret=%r config=%r hash=%r" % (name, secret, settings, 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, settings, hash)) count += 1 log.debug("%s: %s: done; elapsed=%r count=%r", self.descriptionPrefix, thread_name, tick() - start, count) def test_78_fuzz_threading(self): """multithreaded fuzz testing -- random password & options using multiple threads run test_77 simultaneously in multiple threads in an attempt to detect any concurrency issues (e.g. the bug fixed by pybcrypt 0.3) """ self.require_TEST_MODE("full") import threading # check if this test should run if self.handler.is_disabled: raise self.skipTest("not applicable") thread_count = self.fuzz_thread_count if thread_count < 1 or self.max_fuzz_time <= 0: raise self.skipTest("disabled by test mode") # buffer to hold errors thrown by threads failed_lock = threading.Lock() failed = [0] # launch threads, all of which run # test_77_fuzz_input(), and see if any errors get thrown. # if hash has concurrency issues, this should reveal it. def wrapper(): try: self.test_77_fuzz_input(threaded=True) except SkipTest: pass except: with failed_lock: failed[0] += 1 raise def launch(n): name = "Fuzz-Thread-%d" % (n,) thread = threading.Thread(target=wrapper, name=name) thread.setDaemon(True) thread.start() return thread threads = [launch(n) for n in irange(thread_count)] # wait until all threads exit timeout = self.max_fuzz_time * thread_count * 4 stalled = 0 for thread in threads: thread.join(timeout) if not thread.is_alive(): continue # XXX: not sure why this is happening, main one seems 1/4 times for sun_md5_crypt log.error("%s timed out after %f seconds", thread.name, timeout) stalled += 1 # if any thread threw an error, raise one ourselves. if failed[0]: raise self.fail("%d/%d threads failed concurrent fuzz testing " "(see error log for details)" % (failed[0], thread_count)) if stalled: raise self.fail("%d/%d threads stalled during concurrent fuzz testing " "(see error log for details)" % (stalled, thread_count)) #--------------------------------------------------------------- # fuzz constants & helpers #--------------------------------------------------------------- @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 @property def fuzz_thread_count(self): """number of threads for threaded fuzz testing""" value = int(os.environ.get("PASSLIB_TEST_FUZZ_THREADS") or 0) if value: return value elif TEST_MODE(max="quick"): return 0 else: return 10 #--------------------------------------------------------------- # fuzz verifiers #--------------------------------------------------------------- #: list of custom fuzz-test verifiers (in addition to hasher itself, #: and backend-specific wrappers of hasher). each element is #: name of method that will return None / a verifier callable. fuzz_verifiers = ("fuzz_verifier_default",) def get_fuzz_verifiers(self, threaded=False): """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 for method_name in self.fuzz_verifiers: func = getattr(self, method_name)() if func is not None: verifiers.append(func) # create verifiers for any other available backends # NOTE: skipping this under threading test, # since backend switching isn't threadsafe (yet) if hasattr(handler, "backends") and TEST_MODE("full") and not threaded: def maker(backend): def func(secret, hash): orig_backend = handler.get_backend() try: handler.set_backend(backend) return handler.verify(secret, hash) finally: handler.set_backend(orig_backend) func.__name__ = "check_" + backend + "_backend" func.__doc__ = backend + "-backend" return func for backend in iter_alt_backends(handler): 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 #--------------------------------------------------------------- # fuzz settings generation #--------------------------------------------------------------- class FuzzHashGenerator(object): """ helper which takes care of generating random passwords & configuration options to test hash with. separate from test class so we can create one per thread. """ #========================================================== # class attrs #========================================================== # alphabet for randomly generated passwords password_alphabet = u('qwertyASDF1234<>.@*#! \u00E1\u0259\u0411\u2113') # encoding when testing bytes password_encoding = "utf-8" # map of setting kwd -> method name. # will ignore setting if method returns None. # subclasses should make copy of dict. settings_map = dict(rounds="random_rounds", salt_size="random_salt_size", ident="random_ident") # map of context kwd -> method name. context_map = {} #========================================================== # init / generation #========================================================== def __init__(self, test, rng): self.test = test self.handler = test.handler self.rng = rng def generate(self): """ generate random password and options for fuzz testing. :returns: `(secret, other_secret, settings_kwds, context_kwds)` """ def gendict(map): out = {} for key, meth in map.items(): func = getattr(self, meth) value = getattr(self, meth)() if value is not None: out[key] = value return out secret, other = self.random_password_pair() return dict(secret=secret, other=other, settings=gendict(self.settings_map), context=gendict(self.context_map), ) #========================================================== # helpers #========================================================== def randintgauss(self, lower, upper, mu, sigma): """generate random int w/ gauss distirbution""" value = self.rng.normalvariate(mu, sigma) return int(limit(value, lower, upper)) #========================================================== # settings generation #========================================================== def random_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 self.randintgauss(lower, upper, default, default*.5) def random_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 self.randintgauss(lower, upper, default, default*.5) def random_ident(self): rng = self.rng 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) return rng.choice(handler.ident_values) #========================================================== # fuzz password generation #========================================================== def random_password_pair(self): """generate random password, and non-matching alternate password""" secret = self.random_password() while True: other = self.random_password() if self.accept_password_pair(secret, other): break rng = self.rng if rng.randint(0,1): secret = secret.encode(self.password_encoding) if rng.randint(0,1): other = other.encode(self.password_encoding) return secret, other def random_password(self): """generate random passwords for fuzz testing""" # occasionally try an empty password rng = self.rng if rng.random() < .0001: return u('') # check if truncate size needs to be considered handler = self.handler truncate_size = handler.truncate_error and handler.truncate_size max_size = truncate_size or 999999 # pick endpoint if max_size < 50 or rng.random() < .5: # chance of small password (~15 chars) size = self.randintgauss(1, min(max_size, 50), 15, 15) else: # otherwise large password (~70 chars) size = self.randintgauss(50, min(max_size, 99), 70, 20) # generate random password result = getrandstr(rng, self.password_alphabet, size) # trim ones that encode past truncate point. if truncate_size and isinstance(result, unicode): while len(result.encode("utf-8")) > truncate_size: result = result[:-1] return result def accept_password_pair(self, secret, other): """verify fuzz pair contains different passwords""" return secret != other #========================================================== # eoc FuzzGenerator #========================================================== #=================================================================== # "disabled hasher" api #=================================================================== def test_disable_and_enable(self): """.disable() / .enable() methods""" # # setup # handler = self.handler if not handler.is_disabled: self.assertFalse(hasattr(handler, "disable")) self.assertFalse(hasattr(handler, "enable")) self.assertFalse(self.disabled_contains_salt) raise self.skipTest("not applicable") # # disable() # # w/o existing hash disabled_default = handler.disable() self.assertIsInstance(disabled_default, str, msg="disable() must return native string") self.assertTrue(handler.identify(disabled_default), msg="identify() didn't recognize disable() result: %r" % (disabled_default)) # w/ existing hash stub = self.getRandom().choice(self.known_other_hashes)[1] disabled_stub = handler.disable(stub) self.assertIsInstance(disabled_stub, str, msg="disable() must return native string") self.assertTrue(handler.identify(disabled_stub), msg="identify() didn't recognize disable() result: %r" % (disabled_stub)) # # enable() # # w/o original hash self.assertRaisesRegex(ValueError, "cannot restore original hash", handler.enable, disabled_default) # w/ original hash try: result = handler.enable(disabled_stub) error = None except ValueError as e: result = None error = e if error is None: # if supports recovery, should have returned stub (e.g. unix_disabled); self.assertIsInstance(result, str, msg="enable() must return native string") self.assertEqual(result, stub) else: # if doesn't, should have thrown appropriate error self.assertIsInstance(error, ValueError) self.assertRegex("cannot restore original hash", str(error)) # # test repeating disable() & salting state # # repeating disabled disabled_default2 = handler.disable() if self.disabled_contains_salt: # should return new salt for each call (e.g. django_disabled) self.assertNotEqual(disabled_default2, disabled_default) elif error is None: # should return same result for each hash, but unique across hashes self.assertEqual(disabled_default2, disabled_default) # repeating same hash ... disabled_stub2 = handler.disable(stub) if self.disabled_contains_salt: # ... should return different string (if salted) self.assertNotEqual(disabled_stub2, disabled_stub) else: # ... should return same string self.assertEqual(disabled_stub2, disabled_stub) # using different hash ... disabled_other = handler.disable(stub + 'xxx') if self.disabled_contains_salt or error is None: # ... should return different string (if salted or hash encoded) self.assertNotEqual(disabled_other, disabled_stub) else: # ... should return same string self.assertEqual(disabled_other, disabled_stub) #=================================================================== # 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. """ #=================================================================== # class attrs #=================================================================== # platforms that are known to support / not support this hash natively. # list of (platform_regex, True|False|None) entries. platform_crypt_support = [] #: flag indicating backend provides a fallback when safe_crypt() can't handle password has_os_crypt_fallback = True #: alternate handler to use when searching for backend to fake safe_crypt() support. alt_safe_crypt_handler = None #=================================================================== # 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._patch_safe_crypt() super(OsCryptMixin, self).setUp() @classmethod def _get_safe_crypt_handler_backend(cls): """ return (handler, backend) pair to use for faking crypt.crypt() support for hash. backend will be None if none availabe. """ # find handler that generates safe_crypt() compatible hash handler = cls.alt_safe_crypt_handler if not handler: handler = unwrap_handler(cls.handler) # hack to prevent recursion issue when .has_backend() is called handler.get_backend() # find backend which isn't os_crypt alt_backend = get_alt_backend(handler, "os_crypt") return handler, alt_backend 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. """ # find handler & backend handler, alt_backend = self._get_safe_crypt_handler_backend() if not alt_backend: raise AssertionError("handler has no available alternate backends!") # create subclass of handler, which we swap to an alternate backend alt_handler = handler.using() alt_handler.set_backend(alt_backend) def crypt_stub(secret, hash): hash = alt_handler.genhash(secret, hash) assert isinstance(hash, str) return hash import passlib.utils as mod self.patchAttr(mod, "_crypt", crypt_stub) self.using_patched_crypt = True @classmethod def _get_skip_backend_reason(cls, backend): """ make sure os_crypt backend is tested when it's known os_crypt will be faked by _patch_safe_crypt() """ assert backend == "os_crypt" reason = super(OsCryptMixin, cls)._get_skip_backend_reason(backend) from passlib.utils import has_crypt if reason == cls.BACKEND_NOT_AVAILABLE and has_crypt: if TEST_MODE("full") and cls._get_safe_crypt_handler_backend()[1]: # in this case, _patch_safe_crypt() will monkeypatch os_crypt # to use another backend, just so we can test os_crypt fully. return None else: return "hash not supported by os crypt()" return reason #=================================================================== # custom tests #=================================================================== # TODO: turn into decorator, and use mock library. def _use_mock_crypt(self): """ patch passlib.utils.safe_crypt() so it returns mock value for duration of test. returns function whose .return_value controls what's returned. this defaults to None. """ import passlib.utils as mod def mock_crypt(secret, config): # let 'test' string through so _load_os_crypt_backend() will still work if secret == "test": return mock_crypt.__wrapped__(secret, config) else: return mock_crypt.return_value mock_crypt.__wrapped__ = mod._crypt mock_crypt.return_value = None self.patchAttr(mod, "_crypt", mock_crypt) return mock_crypt def test_80_faulty_crypt(self): """test with faulty crypt()""" hash = self.get_sample_hash()[1] exc_types = (AssertionError,) mock_crypt = self._use_mock_crypt() def test(value): # set safe_crypt() to return specified value, and # make sure assertion error is raised by handler. mock_crypt.return_value = 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""" # mock up safe_crypt to return None mock_crypt = self._use_mock_crypt() mock_crypt.return_value = None if self.has_os_crypt_fallback: # handler should have a fallback to use when os_crypt backend refuses to handle secret. 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)) #=================================================================== # fuzzy verified support -- add new verified that uses os crypt() #=================================================================== def fuzz_verifier_crypt(self): """test results against OS crypt()""" # don't use this if we're faking safe_crypt (pointless test), # or if handler is a wrapper (only original handler will be supported by os) handler = self.handler if self.using_patched_crypt or hasattr(handler, "wrapped"): return None # create a wrapper for fuzzy verified to use from crypt import crypt encoding = self.FuzzHashGenerator.password_encoding def check_crypt(secret, hash): """stdlib-crypt""" if not self.crypt_supports_variant(hash): return "skip" secret = to_native_str(secret, encoding) return crypt(secret, hash) == hash return check_crypt def crypt_supports_variant(self, hash): """ fuzzy_verified_crypt() helper -- used to determine if os crypt() supports a particular hash variant. """ return True #=================================================================== # 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 hash/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.hash(password, user=self.default_user) if self.requires_user: self.assertRaises(TypeError, handler.hash, 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.hash(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', context=dict(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_stub_encrypt() 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 #=================================================================== class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): context_map = HandlerCase.FuzzHashGenerator.context_map.copy() context_map.update(user="random_user") user_alphabet = u("asdQWE123") def random_user(self): rng = self.rng if not self.test.requires_user and rng.random() < .1: return None return getrandstr(rng, self.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 hash/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"), ] class FuzzHashGenerator(HandlerCase.FuzzHashGenerator): 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(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: backup = self._orig_registry = {} for name, mod in list(sys.modules.items()): if mod is None or not pattern.match(name): continue reg = getattr(mod, "__warningregistry__", None) if reg: backup[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 registry backup, clearing all registry entries that we didn't archive backup = self._orig_registry for name, mod in list(sys.modules.items()): if mod is None or not pattern.match(name): continue reg = getattr(mod, "__warningregistry__", None) if reg: reg.clear() orig = backup.get(name) if orig: if reg is None: setattr(mod, "__warningregistry__", orig) else: reg.update(orig) super(reset_warnings, self).__exit__(*exc_info) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/sample_config_1s.cfg0000644000175000017500000000035612214647077023012 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.7.1/passlib/tests/test_crypto_builtin_md4.py0000644000175000017500000001303413015205366024327 0ustar biscuitbiscuit00000000000000"""passlib.tests -- unittests for passlib.crypto._md4""" #============================================================================= # imports #============================================================================= from __future__ import with_statement, division # core from binascii import hexlify import hashlib # site # pkg # module from passlib.utils.compat import bascii_to_str, PY3, u from passlib.crypto.digest import lookup_hash from passlib.tests.utils import TestCase, skipUnless # local __all__ = [ "_Common_MD4_Test", "MD4_Builtin_Test", "MD4_SSL_Test", ] #============================================================================= # test pure-python MD4 implementation #============================================================================= class _Common_MD4_Test(TestCase): """common code for testing md4 backends""" 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 get_md4_const(self): """ get md4 constructor -- overridden by subclasses to use alternate backends. """ return lookup_hash("md4").const def test_attrs(self): """informational attributes""" h = self.get_md4_const()() self.assertEqual(h.name, "md4") self.assertEqual(h.digest_size, 16) self.assertEqual(h.block_size, 64) def test_md4_update(self): """update() method""" md4 = self.get_md4_const() h = md4(b'') self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") h.update(b'a') self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") h.update(b'bcdefghijklmnopqrstuvwxyz') self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9") if PY3: # reject unicode, hash should return digest of b'' h = md4() self.assertRaises(TypeError, h.update, u('a')) self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") else: # coerce unicode to ascii, hash should return digest of b'a' h = md4() h.update(u('a')) self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") def test_md4_hexdigest(self): """hexdigest() method""" md4 = self.get_md4_const() for input, hex in self.vectors: out = md4(input).hexdigest() self.assertEqual(out, hex) def test_md4_digest(self): """digest() method""" md4 = self.get_md4_const() for input, hex in self.vectors: out = bascii_to_str(hexlify(md4(input).digest())) self.assertEqual(out, hex) def test_md4_copy(self): """copy() method""" md4 = self.get_md4_const() 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 various backends #------------------------------------------------------------------------ def has_native_md4(): # pragma: no cover -- runtime detection """ check if hashlib natively supports md4. """ try: hashlib.new("md4") return True except ValueError: # not supported - ssl probably missing (e.g. ironpython) return False @skipUnless(has_native_md4(), "hashlib lacks ssl/md4 support") class MD4_SSL_Test(_Common_MD4_Test): descriptionPrefix = "hashlib.new('md4')" # NOTE: we trust ssl got md4 implementation right, # this is more to test our test is correct :) def setUp(self): super(MD4_SSL_Test, self).setUp() # make sure we're using right constructor. self.assertEqual(self.get_md4_const().__module__, "hashlib") class MD4_Builtin_Test(_Common_MD4_Test): descriptionPrefix = "passlib.crypto._md4.md4()" def setUp(self): super(MD4_Builtin_Test, self).setUp() if has_native_md4(): # Temporarily make lookup_hash() use builtin pure-python implementation, # by monkeypatching hashlib.new() to ensure we fall back to passlib's md4 class. orig = hashlib.new def wrapper(name, *args): if name == "md4": raise ValueError("md4 disabled for testing") return orig(name, *args) self.patchAttr(hashlib, "new", wrapper) # flush cache before & after test, since we're mucking with it. lookup_hash.clear_cache() self.addCleanup(lookup_hash.clear_cache) # make sure we're using right constructor. self.assertEqual(self.get_md4_const().__module__, "passlib.crypto._md4") #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_hosts.py0000644000175000017500000000750213015205366021660 0ustar biscuitbiscuit00000000000000"""test passlib.hosts""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core import logging; log = logging.getLogger(__name__) # 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.7.1/passlib/tests/tox_support.py0000644000175000017500000000465113015205366022071 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.7.1/passlib/tests/test_win32.py0000644000175000017500000000360013015205366021455 0ustar biscuitbiscuit00000000000000"""tests for passlib.win32 -- (c) Assurance Technologies 2003-2009""" #============================================================================= # imports #============================================================================= # core 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.7.1/passlib/tests/test_ext_django.py0000644000175000017500000010005313016615126022635 0ustar biscuitbiscuit00000000000000"""test passlib.ext.django""" #============================================================================= # imports #============================================================================= # core from __future__ import absolute_import, division, print_function import logging; log = logging.getLogger(__name__) import sys # site # pkg from passlib import apps as _apps, exc, registry from passlib.apps import django10_context, django14_context, django16_context from passlib.context import CryptContext from passlib.ext.django.utils import ( DJANGO_VERSION, MIN_DJANGO_VERSION, DjangoTranslator, ) from passlib.utils.compat import iteritems, get_method_function, u from passlib.utils.decor import memoized_property # tests from passlib.tests.utils import TestCase, TEST_MODE, handler_derived_from from passlib.tests.test_handlers import get_handler_case, conditionally_available_hashes # local __all__ = [ "DjangoBehaviorTest", "ExtensionBehaviorTest", "_ExtensionSupport", ] #============================================================================= # configure django settings for testcases #============================================================================= # whether we have supported django version has_min_django = DJANGO_VERSION >= MIN_DJANGO_VERSION # import and configure empty django settings # NOTE: we don't want to set up entirety of django, so not using django.setup() directly. # instead, manually configuring the settings, and setting it up w/ no apps installed. # in future, may need to alter this so we call django.setup() after setting # DJANGO_SETTINGS_MODULE to a custom settings module w/ a dummy django app. if has_min_django: # # initialize django settings manually # 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 not settings.configured: settings.configure() # # init django apps w/ NO installed apps. # NOTE: required for django >= 1.9 # from django.apps import apps apps.populate(["django.contrib.contenttypes", "django.contrib.auth"]) #============================================================================= # 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): delattr(settings, k) else: setattr(settings, k, v) if has_min_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. # NOTE: .Meta.app_label required for django >= 1.9 class Meta: app_label = __name__ @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 #============================================================================= # build config dict that matches stock django # XXX: move these to passlib.apps? if DJANGO_VERSION >= (1, 10): stock_config = _apps.django110_context.to_dict() stock_rounds = 30000 elif DJANGO_VERSION >= (1, 9): stock_config = _apps.django16_context.to_dict() stock_rounds = 24000 else: # 1.8 stock_config = _apps.django16_context.to_dict() stock_rounds = 20000 stock_config.update( deprecated="auto", django_pbkdf2_sha1__default_rounds=stock_rounds, django_pbkdf2_sha256__default_rounds=stock_rounds, ) # override sample hashes used in test cases from passlib.hash import django_pbkdf2_sha256 sample_hashes = dict( django_pbkdf2_sha256=("not a password", django_pbkdf2_sha256 .using(rounds=stock_config.get("django_pbkdf2_sha256__default_rounds")) .hash("not a password")) ) #============================================================================= # 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, hashers user_attrs = ["check_password", "set_password"] model_attrs = ["check_password", "make_password"] hasher_attrs = ["check_password", "make_password", "get_hasher", "identify_hasher", "get_hashers"] objs = [(models, model_attrs), (models.User, user_attrs), (hashers, hasher_attrs), ] 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.adapter.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.adapter.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.utils", "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.adapter.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 DJANGO_VERSION: raise self.skipTest("Django not installed") elif not has_min_django: raise self.skipTest("Django version too old") # 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""" self.assertTrue(user.password.startswith("!")) 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) self.assertTrue(user.has_usable_password(), "hash should be usable: %r" % (user.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" from django.contrib.auth.hashers import (check_password, make_password, is_password_usable, identify_hasher) #======================================================= # 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 from django.contrib.auth.models import check_password as check_password2 self.assertEqual(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 hash = make_password(PASS1) self.assertTrue(ctx.handler().verify(PASS1, hash)) # check_password() - n/a #======================================================= # empty password behavior #======================================================= # 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 #======================================================= # 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) self.assert_unusable_password(user) # User.check_password() should always fail 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 self.assertTrue(make_password(None).startswith("!")) # check_password() should return False (didn't handle disabled under 1.3) self.assertFalse(check_password(PASS1, '!')) # identify_hasher() and is_password_usable() should reject it self.assertFalse(is_password_usable(user.password)) self.assertRaises(ValueError, identify_hasher, user.password) #======================================================= # hash=None #======================================================= # User.set_password() - n/a # User.check_password() - returns False user = FakeUser() user.password = None self.assertFalse(user.check_password(PASS1)) self.assertFalse(user.has_usable_password()) # make_password() - n/a # check_password() - error self.assertFalse(check_password(PASS1, None)) # identify_hasher() - error 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() # As of django 1.5, blank OR invalid hash returns False user = FakeUser() user.password = hash self.assertFalse(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() self.assertFalse(user.has_usable_password()) # make_password() - n/a # check_password() self.assertFalse(check_password(PASS1, hash)) # identify_hasher() - throws error 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.handler(scheme).deprecated assert not deprecated or scheme != ctx.default_scheme() try: testcase = get_handler_case(scheme) except exc.MissingBackendError: assert scheme in conditionally_available_hashes continue assert handler_derived_from(handler, testcase.handler) if handler.is_disabled: continue if not registry.has_backend(handler): # TODO: move this above get_handler_case(), # and omit MissingBackendError check. assert scheme in ["django_bcrypt", "django_bcrypt_sha256", "django_argon2"], \ "%r scheme should always have active backend" % scheme continue try: secret, hash = sample_hashes[scheme] except KeyError: get_sample_hash = testcase("setUp").get_sample_hash while True: secret, hash = get_sample_hash() if secret: # don't select blank passwords break other = 'dontletmein' # User.set_password() - n/a #------------------------------------------------------- # User.check_password()+migration against known hash #------------------------------------------------------- user = FakeUser() user.password = hash # check against invalid password self.assertFalse(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 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 #------------------------------------------------------- alg = DjangoTranslator().passlib_to_django_name(scheme) hash2 = make_password(secret, hasher=alg) self.assertTrue(handler.verify(secret, hash2)) #------------------------------------------------------- # check_password()+setter against known hash #------------------------------------------------------- # 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 (feature we don't currently support fully) ##self.assertTrue(check_password(secret, hash, setter=setter, preferred='fooey')) ##self.assertEqual(setter.popstate(), [secret]) # TODO: get_hasher() #------------------------------------------------------- # identify_hasher() recognizes known hash #------------------------------------------------------- self.assertTrue(is_password_usable(hash)) name = DjangoTranslator().django_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 adapter 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"): adapter._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"): adapter._manager.check_all() models.check_password = orig def test_02_handler_wrapper(self): """test Hasher-compatible handler wrappers""" from django.contrib.auth import hashers passlib_to_django = DjangoTranslator().passlib_to_django # should return native django hasher if available if DJANGO_VERSION > (1,10): self.assertRaises(ValueError, passlib_to_django, "hex_md5") else: hasher = passlib_to_django("hex_md5") self.assertIsInstance(hasher, hashers.UnsaltedMD5PasswordHasher) hasher = passlib_to_django("django_bcrypt") self.assertIsInstance(hasher, hashers.BCryptPasswordHasher) # otherwise should return wrapper from passlib.hash import sha256_crypt hasher = passlib_to_django("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, rounds=1234) self.assertEqual(encoded, "$5$rounds=1234$abcdabcdabcdabcd$" "v2RWkZQzctPdejyRqmmTDQpZN6wTh7.RUy9zF2LftT6") self.assertEqual(hasher.safe_summary(encoded), {'algorithm': 'sha256_crypt', 'salt': u('abcdab**********'), 'rounds': 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) ctx = django16_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 #=================================================================== #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_crypto_scrypt.py0000644000175000017500000006206713043701620023446 0ustar biscuitbiscuit00000000000000"""tests for passlib.utils.scrypt""" #============================================================================= # imports #============================================================================= # core from binascii import hexlify import hashlib import logging; log = logging.getLogger(__name__) import struct import warnings warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*") # site # pkg from passlib import exc from passlib.utils import getrandbytes from passlib.utils.compat import PYPY, u, bascii_to_str from passlib.utils.decor import classproperty from passlib.tests.utils import TestCase, skipUnless, TEST_MODE, hb # subject from passlib.crypto import scrypt as scrypt_mod # local __all__ = [ "ScryptEngineTest", "BuiltinScryptTest", "FastScryptTest", ] #============================================================================= # support functions #============================================================================= def hexstr(data): """return bytes as hex str""" return bascii_to_str(hexlify(data)) def unpack_uint32_list(data, check_count=None): """unpack bytes as list of uint32 values""" count = len(data) // 4 assert check_count is None or check_count == count return struct.unpack("<%dI" % count, data) def seed_bytes(seed, count): """ generate random reference bytes from specified seed. used to generate some predictable test vectors. """ if hasattr(seed, "encode"): seed = seed.encode("ascii") buf = b'' i = 0 while len(buf) < count: buf += hashlib.sha256(seed + struct.pack("" % cls.backend backend = None #============================================================================= # setup #============================================================================= def setUp(self): assert self.backend scrypt_mod._set_backend(self.backend) super(_CommonScryptTest, self).setUp() #============================================================================= # reference vectors #============================================================================= reference_vectors = [ # entry format: (secret, salt, n, r, p, keylen, result) #------------------------------------------------------------------------ # test vectors from scrypt whitepaper -- # http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b # # also present in (expired) scrypt rfc draft -- # https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 11 #------------------------------------------------------------------------ ("", "", 16, 1, 1, 64, hb(""" 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97 f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42 fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17 e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06 """)), ("password", "NaCl", 1024, 8, 16, 64, hb(""" fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40 """)), # NOTE: the following are skipped for all backends unless TEST_MODE="full" ("pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, hb(""" 70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2 d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9 e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87 """)), # NOTE: the following are always skipped for the builtin backend, # (just takes too long to be worth it) ("pleaseletmein", "SodiumChloride", 1048576, 8, 1, 64, hb(""" 21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81 ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47 8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3 37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4 """)), ] def test_reference_vectors(self): """reference vectors""" for secret, salt, n, r, p, keylen, result in self.reference_vectors: if n >= 1024 and TEST_MODE(max="default"): # skip large values unless we're running full test suite continue if n > 16384 and self.backend == "builtin": # skip largest vector for builtin, takes WAAY too long # (46s under pypy, ~5m under cpython) continue log.debug("scrypt reference vector: %r %r n=%r r=%r p=%r", secret, salt, n, r, p) self.assertEqual(scrypt_mod.scrypt(secret, salt, n, r, p, keylen), result) #============================================================================= # fuzz testing #============================================================================= _already_tested_others = None def test_other_backends(self): """compare output to other backends""" # only run once, since test is symetric. # maybe this means it should go somewhere else? if self._already_tested_others: raise self.skipTest("already run under %r backend test" % self._already_tested_others) self._already_tested_others = self.backend rng = self.getRandom() # get available backends orig = scrypt_mod.backend available = set(name for name in scrypt_mod.backend_values if scrypt_mod._has_backend(name)) scrypt_mod._set_backend(orig) available.discard(self.backend) if not available: raise self.skipTest("no other backends found") warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend", category=exc.PasslibSecurityWarning) # generate some random options, and cross-check output for _ in range(10): # NOTE: keeping values low due to builtin test secret = getrandbytes(rng, rng.randint(0, 64)) salt = getrandbytes(rng, rng.randint(0, 64)) n = 1 << rng.randint(1, 10) r = rng.randint(1, 8) p = rng.randint(1, 3) ks = rng.randint(1, 64) previous = None backends = set() for name in available: scrypt_mod._set_backend(name) self.assertNotIn(scrypt_mod._scrypt, backends) backends.add(scrypt_mod._scrypt) result = hexstr(scrypt_mod.scrypt(secret, salt, n, r, p, ks)) self.assertEqual(len(result), 2*ks) if previous is not None: self.assertEqual(result, previous, msg="%r output differs from others %r: %r" % (name, available, [secret, salt, n, r, p, ks])) #============================================================================= # test input types #============================================================================= def test_backend(self): """backend management""" # clobber backend scrypt_mod.backend = None scrypt_mod._scrypt = None self.assertRaises(TypeError, scrypt_mod.scrypt, 's', 's', 2, 2, 2, 16) # reload backend scrypt_mod._set_backend(self.backend) self.assertEqual(scrypt_mod.backend, self.backend) scrypt_mod.scrypt('s', 's', 2, 2, 2, 16) # throw error for unknown backend self.assertRaises(ValueError, scrypt_mod._set_backend, 'xxx') self.assertEqual(scrypt_mod.backend, self.backend) def test_secret_param(self): """'secret' parameter""" def run_scrypt(secret): return hexstr(scrypt_mod.scrypt(secret, "salt", 2, 2, 2, 16)) # unicode TEXT = u("abc\u00defg") self.assertEqual(run_scrypt(TEXT), '05717106997bfe0da42cf4779a2f8bd8') # utf8 bytes TEXT_UTF8 = b'abc\xc3\x9efg' self.assertEqual(run_scrypt(TEXT_UTF8), '05717106997bfe0da42cf4779a2f8bd8') # latin1 bytes TEXT_LATIN1 = b'abc\xdefg' self.assertEqual(run_scrypt(TEXT_LATIN1), '770825d10eeaaeaf98e8a3c40f9f441d') # accept empty string self.assertEqual(run_scrypt(""), 'ca1399e5fae5d3b9578dcd2b1faff6e2') # reject other types self.assertRaises(TypeError, run_scrypt, None) self.assertRaises(TypeError, run_scrypt, 1) def test_salt_param(self): """'salt' parameter""" def run_scrypt(salt): return hexstr(scrypt_mod.scrypt("secret", salt, 2, 2, 2, 16)) # unicode TEXT = u("abc\u00defg") self.assertEqual(run_scrypt(TEXT), 'a748ec0f4613929e9e5f03d1ab741d88') # utf8 bytes TEXT_UTF8 = b'abc\xc3\x9efg' self.assertEqual(run_scrypt(TEXT_UTF8), 'a748ec0f4613929e9e5f03d1ab741d88') # latin1 bytes TEXT_LATIN1 = b'abc\xdefg' self.assertEqual(run_scrypt(TEXT_LATIN1), '91d056fb76fb6e9a7d1cdfffc0a16cd1') # reject other types self.assertRaises(TypeError, run_scrypt, None) self.assertRaises(TypeError, run_scrypt, 1) def test_n_param(self): """'n' (rounds) parameter""" def run_scrypt(n): return hexstr(scrypt_mod.scrypt("secret", "salt", n, 2, 2, 16)) # must be > 1, and a power of 2 self.assertRaises(ValueError, run_scrypt, -1) self.assertRaises(ValueError, run_scrypt, 0) self.assertRaises(ValueError, run_scrypt, 1) self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') self.assertRaises(ValueError, run_scrypt, 3) self.assertRaises(ValueError, run_scrypt, 15) self.assertEqual(run_scrypt(16), '0272b8fc72bc54b1159340ed99425233') def test_r_param(self): """'r' (block size) parameter""" def run_scrypt(r, n=2, p=2): return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16)) # must be > 1 self.assertRaises(ValueError, run_scrypt, -1) self.assertRaises(ValueError, run_scrypt, 0) self.assertEqual(run_scrypt(1), '3d630447d9f065363b8a79b0b3670251') self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') self.assertEqual(run_scrypt(5), '114f05e985a903c27237b5578e763736') # reject r*p >= 2**30 self.assertRaises(ValueError, run_scrypt, (1<<30), p=1) self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, p=2) def test_p_param(self): """'p' (parallelism) parameter""" def run_scrypt(p, n=2, r=2): return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16)) # must be > 1 self.assertRaises(ValueError, run_scrypt, -1) self.assertRaises(ValueError, run_scrypt, 0) self.assertEqual(run_scrypt(1), 'f2960ea8b7d48231fcec1b89b784a6fa') self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66') self.assertEqual(run_scrypt(5), '848a0eeb2b3543e7f543844d6ca79782') # reject r*p >= 2**30 self.assertRaises(ValueError, run_scrypt, (1<<30), r=1) self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, r=2) def test_keylen_param(self): """'keylen' parameter""" rng = self.getRandom() def run_scrypt(keylen): return hexstr(scrypt_mod.scrypt("secret", "salt", 2, 2, 2, keylen)) # must be > 0 self.assertRaises(ValueError, run_scrypt, -1) self.assertRaises(ValueError, run_scrypt, 0) self.assertEqual(run_scrypt(1), 'da') # pick random value ksize = rng.randint(0, 1 << 10) self.assertEqual(len(run_scrypt(ksize)), 2*ksize) # 2 hex chars per output # one more than upper bound self.assertRaises(ValueError, run_scrypt, ((2**32) - 1) * 32 + 1) #============================================================================= # eoc #============================================================================= # NOTE: builtin version runs VERY slow (except under PyPy, where it's only 11x slower), # so skipping under quick test mode. @skipUnless(PYPY or TEST_MODE(min="default"), "skipped under current test mode") class BuiltinScryptTest(_CommonScryptTest): backend = "builtin" def setUp(self): super(BuiltinScryptTest, self).setUp() warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend", category=exc.PasslibSecurityWarning) def test_missing_backend(self): """backend management -- missing backend""" if _can_import_scrypt(): raise self.skipTest("'scrypt' backend is present") self.assertRaises(exc.MissingBackendError, scrypt_mod._set_backend, 'scrypt') def _can_import_scrypt(): """check if scrypt package is importable""" try: import scrypt except ImportError as err: if "scrypt" in str(err): return False raise return True @skipUnless(_can_import_scrypt(), "'scrypt' package not found") class ScryptPackageTest(_CommonScryptTest): backend = "scrypt" def test_default_backend(self): """backend management -- default backend""" scrypt_mod._set_backend("default") self.assertEqual(scrypt_mod.backend, "scrypt") #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_registry.py0000644000175000017500000002236313015205366022372 0ustar biscuitbiscuit00000000000000"""tests for passlib.hash -- (c) Assurance Technologies 2003-2009""" #============================================================================= # imports #============================================================================= from __future__ import with_statement # core from logging import getLogger 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 # 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 warnings.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 warnings.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, conditionally_available_hashes 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 conditionally_available_hashes: # expected to fail on some setups continue raise #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/sample1c.cfg0000644000175000017500000000075213015205366021275 0ustar biscuitbiscuit00000000000000ÿþ[mypolicy] schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt default = md5_crypt all__vary_rounds = 0.1 bsdi_crypt__default_rounds = 25001 bsdi_crypt__max_rounds = 30001 sha512_crypt__max_rounds = 50000 sha512_crypt__min_rounds = 40000 passlib-1.7.1/passlib/tests/__main__.py0000644000175000017500000000012212214647077021201 0ustar biscuitbiscuit00000000000000import os from nose import run run( defaultTest=os.path.dirname(__file__), ) passlib-1.7.1/passlib/tests/test_crypto_des.py0000644000175000017500000002125213015205366022671 0ustar biscuitbiscuit00000000000000"""passlib.tests -- unittests for passlib.crypto.des""" #============================================================================= # imports #============================================================================= from __future__ import with_statement, division # core from functools import partial # site # pkg # module from passlib.utils import getrandbytes from passlib.tests.utils import TestCase #============================================================================= # test DES routines #============================================================================= class DesTest(TestCase): descriptionPrefix = "passlib.crypto.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): """expand_des_key()""" from passlib.crypto.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): """shrink_des_key()""" from passlib.crypto.des import expand_des_key, shrink_des_key, INT_64_MASK rng = self.getRandom() # make sure reverse works for some random keys # uses bytes, ints are tested under # 01 for i in range(20): key1 = getrandbytes(rng, 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.crypto.des import _KDATA_MASK, _KPARITY_MASK, INT_64_MASK rng = self.getRandom() return (key & _KDATA_MASK) | (rng.randint(0,INT_64_MASK) & _KPARITY_MASK) def test_03_encrypt_bytes(self): """des_encrypt_block()""" from passlib.crypto.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): """des_encrypt_int_block()""" from passlib.crypto.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) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/tests/test_context_deprecated.py0000644000175000017500000007114213015205366024365 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 from logging import getLogger import os import warnings # 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.utils import to_bytes, to_unicode import passlib.utils.handlers as uh from passlib.tests.utils import TestCase, set_file from passlib.registry import (register_crypt_handler_path, _has_crypt_handler as has_crypt_handler, _unload_handler_name as unload_handler_name, ) # 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") warnings.filterwarnings("ignore", "The 'all' scheme is deprecated.*") warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd") 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(), 0) self.assertEqual(pb.get_min_verify_time('admin'), 0) #=================================================================== # 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.hash, '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.hash, '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""" warnings.filterwarnings("ignore", ".*needs_update.*'scheme' keyword is deprecated.*") # # 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, {}), # NOTE: 'scheme' kwd is deprecated... (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.7.1/passlib/tests/test_context.py0000644000175000017500000022243713043457152022215 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 datetime from functools import partial import logging; log = logging.getLogger(__name__) import os import warnings # site # pkg from passlib import hash from passlib.context import CryptContext, LazyCryptContext from passlib.exc import PasslibConfigWarning, PasslibHashWarning from passlib.utils import tick, to_unicode from passlib.utils.compat import irange, u, unicode, str_to_uascii, PY2, PY26 import passlib.utils.handlers as uh from passlib.tests.utils import (TestCase, set_file, TICK_RESOLUTION, quicksleep, time_call, handler_derived_from) 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 = 30001, bsdi_crypt__default_rounds = 25001, 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%% bsdi_crypt__default_rounds = 25001 bsdi_crypt__max_rounds = 30001 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 = 25001 bsdi_crypt__max_rounds = 30001 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 = 29001, bsdi_crypt__max_rounds = 35001, bsdi_crypt__default_rounds = 31001, # using this to test partial replacement of existing options sha512_crypt__min_rounds=45000, ) sample_2_unicode = """\ [passlib] bsdi_crypt__min_rounds = 29001 bsdi_crypt__max_rounds = 35001 bsdi_crypt__default_rounds = 31001 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 = 31, 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, ) #=================================================================== # setup #=================================================================== def setUp(self): super(CryptContextTest, self).setUp() warnings.filterwarnings("ignore", "The 'all' scheme is deprecated.*") warnings.filterwarnings("ignore", ".*'scheme' keyword is deprecated as of Passlib 1.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) # NOTE: "0x-1234" format used by Pyston 0.5.1 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.setting_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.handler(name, category).deprecated] # 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 assertHandlerDerivedFrom(self, handler, base, msg=None): self.assertTrue(handler_derived_from(handler, base), msg=msg) 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, unconfigured=True), tuple(self.sample_1_handlers)) for result, correct in zip(ctx.schemes(resolve=True), self.sample_1_handlers): self.assertTrue(handler_derived_from(result, correct)) # 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, unconfigured=True), hash.md5_crypt) self.assertHandlerDerivedFrom(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(unconfigured=True), hash.md5_crypt) self.assertHandlerDerivedFrom(ctx.handler(), hash.md5_crypt) # by name self.assertEqual(ctx.handler("des_crypt", unconfigured=True), hash.des_crypt) self.assertHandlerDerivedFrom(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(unconfigured=True), hash.sha256_crypt) self.assertHandlerDerivedFrom(ctx.handler(), hash.sha256_crypt) self.assertEqual(ctx.handler(category="staff", unconfigured=True), hash.sha256_crypt) self.assertHandlerDerivedFrom(ctx.handler(category="staff"), hash.sha256_crypt) self.assertEqual(ctx.handler(category="admin", unconfigured=True), hash.md5_crypt) self.assertHandlerDerivedFrom(ctx.handler(category="staff"), hash.sha256_crypt) # test unicode category strings are accepted under py2 if PY2: self.assertEqual(ctx.handler(category=u("staff"), unconfigured=True), hash.sha256_crypt) self.assertEqual(ctx.handler(category=u("admin"), unconfigured=True), 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( truncate_error=True, 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 # NOTE: 'truncate_error' shouldn't be passed along... # 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 # NOTE: vary_rounds shouldn't be passed along... # self.assertEqual(options(cc4, "des_crypt"), dict( deprecated=True, truncate_error=True, )) self.assertEqual(options(cc4, "des_crypt", "user"), dict( deprecated=True, # unconfigured category inherits from default truncate_error=True, )) self.assertEqual(options(cc4, "des_crypt", "admin"), dict( deprecated=True, # unchanged though overidden truncate_error=True, )) # # 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 doesn't use OrderedDict, # making to_string()'s ordering unpredictable... # so we skip this test under PY26. if not PY26: 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 hash/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.hash("test", scheme=crypt.name) self.assertEqual(cc.identify(h), crypt.name) self.assertEqual(cc.identify(h, resolve=True, unconfigured=True), crypt) self.assertHandlerDerivedFrom(cc.identify(h, resolve=True), crypt) self.assertTrue(cc.verify('test', h)) self.assertFalse(cc.verify('notest', h)) # test default h = cc.hash("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........22zGEuacuPOqEpYPDeR0R/', # NOTE: config string generated w/ rounds=1 ) #-------------------------------------------------------------- # border cases #-------------------------------------------------------------- # test unicode category strings are accepted under py2 # this tests basic _get_record() used by hash/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.hash('stub') for secret, kwds in self.nonstring_vectors: self.assertRaises(TypeError, cc.genhash, secret, hash, **kwds) # rejects non-string config strings cc = CryptContext(["des_crypt"]) for config, kwds in self.nonstring_vectors: if hash is None: # NOTE: as of 1.7, genhash is just wrapper for hash(), # and handles genhash(secret, None) fine. continue self.assertRaises(TypeError, cc.genhash, 'secret', config, **kwds) # rejects config=None, even if default scheme lacks config string cc = CryptContext(["mysql323"]) self.assertRaises(TypeError, cc.genhash, "stub", None) # 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_hash(self,): """test hash() method""" # XXX: what more can we test here that isn't deprecated # or handled under another test (e.g. context kwds?) # respects rounds cc = CryptContext(**self.sample_4_dict) hash = cc.hash("password") self.assertTrue(hash.startswith("$5$rounds=3000$")) self.assertTrue(cc.verify("password", hash)) self.assertFalse(cc.verify("passwordx", hash)) # make default > max throws error if attempted # XXX: move this to copy() test? self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=4000) # rejects non-string secrets cc = CryptContext(["des_crypt"]) for secret, kwds in self.nonstring_vectors: self.assertRaises(TypeError, cc.hash, secret, **kwds) # throws error without schemes self.assertRaises(KeyError, CryptContext().hash, 'secret') # bad category values self.assertRaises(TypeError, cc.hash, 'secret', category=1) def test_43_hash_legacy(self, use_16_legacy=False): """test hash() method -- legacy 'scheme' and settings keywords""" cc = CryptContext(**self.sample_4_dict) # TODO: should migrate these tests elsewhere, or remove them. # can be replaced with following equivalent: # # def wrapper(secret, scheme=None, category=None, **kwds): # handler = cc.handler(scheme, category) # if kwds: # handler = handler.using(**kwds) # return handler.hash(secret) # # need to make sure bits being tested here are tested # under the tests for the equivalent methods called above, # and then discard the rest of these under 2.0. # hash specific settings with self.assertWarningList(["passing settings to.*is deprecated"]): self.assertEqual( cc.hash("password", scheme="phpass", salt='.'*8), '$H$5........De04R5Egz0aq8Tf.1eVhY/', ) with self.assertWarningList(["passing settings to.*is deprecated"]): self.assertEqual( cc.hash("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(["passing settings to.*is deprecated"]): self.assertEqual( cc.hash("password", rounds=1999, salt="nacl"), '$5$rounds=1999$nacl$nmfwJIxqj0csloAAvSER0B8LU0ERCAbhmMug4Twl609', ) with self.assertWarningList(["passing settings to.*is deprecated"]): self.assertEqual( cc.hash("password", rounds=2001, salt="nacl"), '$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31' ) # NOTE: max rounds, etc tested in genconfig() # bad scheme values self.assertRaises(KeyError, cc.hash, 'secret', scheme="fake") # XXX: should this be ValueError? self.assertRaises(TypeError, cc.hash, 'secret', scheme=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.hash("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.hash('stub') for secret, kwds in self.nonstring_vectors: self.assertRaises(TypeError, cc.verify, secret, h, **kwds) # always treat hash=None as False self.assertFalse(cc.verify(secret, None)) # rejects non-string hashes cc = CryptContext(["des_crypt"]) for h, kwds in self.nonstring_vectors: if h is None: continue 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 hash.needs_update() interface #-------------------------------------------------------------- check_state = [] class dummy(uh.StaticHandler): name = 'dummy' _hash_prefix = '@' @classmethod def needs_update(cls, hash, secret=None): 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()) # calling needs_update should query callback ctx = CryptContext([dummy]) hash = refhash = dummy.hash("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.handler("des_crypt").hash("password") h2 = cc.handler("sha256_crypt").hash("password") # 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.hash('stub') for secret, kwds in self.nonstring_vectors: self.assertRaises(TypeError, cc.verify_and_update, secret, hash, **kwds) # always treat hash=None as False self.assertEqual(cc.verify_and_update(secret, None), (False, None)) # rejects non-string hashes cc = CryptContext(["des_crypt"]) for hash, kwds in self.nonstring_vectors: if hash is None: continue 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) def test_48_context_kwds(self): """hash(), verify(), and verify_and_update() -- discard unused context keywords""" # setup test case # NOTE: postgres_md5 hash supports 'user' context kwd, which is used for this test. from passlib.hash import des_crypt, md5_crypt, postgres_md5 des_hash = des_crypt.hash("stub") pg_root_hash = postgres_md5.hash("stub", user="root") pg_admin_hash = postgres_md5.hash("stub", user="admin") #------------------------------------------------------------ # case 1: contextual kwds not supported by any hash in CryptContext #------------------------------------------------------------ cc1 = CryptContext([des_crypt, md5_crypt]) self.assertEqual(cc1.context_kwds, set()) # des_scrypt should work w/o any contextual kwds self.assertTrue(des_crypt.identify(cc1.hash("stub")), "des_crypt") self.assertTrue(cc1.verify("stub", des_hash)) self.assertEqual(cc1.verify_and_update("stub", des_hash), (True, None)) # des_crypt should throw error due to unknown context keyword with self.assertWarningList(["passing settings to.*is deprecated"]): self.assertRaises(TypeError, cc1.hash, "stub", user="root") self.assertRaises(TypeError, cc1.verify, "stub", des_hash, user="root") self.assertRaises(TypeError, cc1.verify_and_update, "stub", des_hash, user="root") #------------------------------------------------------------ # case 2: at least one contextual kwd supported by non-default hash #------------------------------------------------------------ cc2 = CryptContext([des_crypt, postgres_md5]) self.assertEqual(cc2.context_kwds, set(["user"])) # verify des_crypt works w/o "user" kwd self.assertTrue(des_crypt.identify(cc2.hash("stub")), "des_crypt") self.assertTrue(cc2.verify("stub", des_hash)) self.assertEqual(cc2.verify_and_update("stub", des_hash), (True, None)) # verify des_crypt ignores "user" kwd self.assertTrue(des_crypt.identify(cc2.hash("stub", user="root")), "des_crypt") self.assertTrue(cc2.verify("stub", des_hash, user="root")) self.assertEqual(cc2.verify_and_update("stub", des_hash, user="root"), (True, None)) # verify error with unknown kwd with self.assertWarningList(["passing settings to.*is deprecated"]): self.assertRaises(TypeError, cc2.hash, "stub", badkwd="root") self.assertRaises(TypeError, cc2.verify, "stub", des_hash, badkwd="root") self.assertRaises(TypeError, cc2.verify_and_update, "stub", des_hash, badkwd="root") #------------------------------------------------------------ # case 3: at least one contextual kwd supported by default hash #------------------------------------------------------------ cc3 = CryptContext([postgres_md5, des_crypt], deprecated="auto") self.assertEqual(cc3.context_kwds, set(["user"])) # postgres_md5 should have error w/o context kwd self.assertRaises(TypeError, cc3.hash, "stub") self.assertRaises(TypeError, cc3.verify, "stub", pg_root_hash) self.assertRaises(TypeError, cc3.verify_and_update, "stub", pg_root_hash) # postgres_md5 should work w/ context kwd self.assertEqual(cc3.hash("stub", user="root"), pg_root_hash) self.assertTrue(cc3.verify("stub", pg_root_hash, user="root")) self.assertEqual(cc3.verify_and_update("stub", pg_root_hash, user="root"), (True, None)) # verify_and_update() should fail against wrong user self.assertEqual(cc3.verify_and_update("stub", pg_root_hash, user="admin"), (False, None)) # verify_and_update() should pass all context kwds through when rehashing self.assertEqual(cc3.verify_and_update("stub", des_hash, user="root"), (True, pg_root_hash)) #=================================================================== # rounds options #=================================================================== # TODO: now that rounds generation has moved out of _CryptRecord to HasRounds, # this should just test that we're passing right options to handler.using(), # and that resulting handler has right settings. # Can then just let HasRounds tests (which are a copy of this) deal with things. # NOTE: the follow tests check how _CryptRecord handles # the min/max/default/vary_rounds options, via the output of # genconfig(). it's assumed hash() takes the same codepath. def test_50_rounds_limits(self): """test rounds limits""" cc = CryptContext(schemes=["sha256_crypt"], sha256_crypt__min_rounds=2000, sha256_crypt__max_rounds=3000, sha256_crypt__default_rounds=2500, ) # stub digest returned by sha256_crypt's genconfig calls.. STUB = '...........................................' #-------------------------------------------------- # settings should have been applied to custom handler, # it should take care of the rest #-------------------------------------------------- custom_handler = cc._get_record("sha256_crypt", None) self.assertEqual(custom_handler.min_desired_rounds, 2000) self.assertEqual(custom_handler.max_desired_rounds, 3000) self.assertEqual(custom_handler.default_rounds, 2500) #-------------------------------------------------- # min_rounds #-------------------------------------------------- # set below handler minimum with self.assertWarningList([PasslibHashWarning]*2): c2 = cc.copy(sha256_crypt__min_rounds=500, sha256_crypt__max_rounds=None, sha256_crypt__default_rounds=500) self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$" + STUB) # below policy minimum # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .replace() with self.assertWarningList([]): self.assertEqual( cc.genconfig(rounds=1999, salt="nacl"), '$5$rounds=1999$nacl$' + STUB) # equal to policy minimum self.assertEqual( cc.genconfig(rounds=2000, salt="nacl"), '$5$rounds=2000$nacl$' + STUB) # above policy minimum self.assertEqual( cc.genconfig(rounds=2001, salt="nacl"), '$5$rounds=2001$nacl$' + STUB) #-------------------------------------------------- # max rounds #-------------------------------------------------- # set above handler max with self.assertWarningList([PasslibHashWarning]*2): c2 = cc.copy(sha256_crypt__max_rounds=int(1e9)+500, sha256_crypt__min_rounds=None, sha256_crypt__default_rounds=int(1e9)+500) self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=999999999$nacl$" + STUB) # above policy max # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using() with self.assertWarningList([]): self.assertEqual( cc.genconfig(rounds=3001, salt="nacl"), '$5$rounds=3001$nacl$' + STUB) # equal policy max self.assertEqual( cc.genconfig(rounds=3000, salt="nacl"), '$5$rounds=3000$nacl$' + STUB) # below policy max self.assertEqual( cc.genconfig(rounds=2999, salt="nacl"), '$5$rounds=2999$nacl$' + STUB) #-------------------------------------------------- # default_rounds #-------------------------------------------------- # explicit default rounds self.assertEqual(cc.genconfig(salt="nacl"), '$5$rounds=2500$nacl$' + STUB) # fallback default rounds - use handler's df = hash.sha256_crypt.default_rounds c2 = cc.copy(sha256_crypt__default_rounds=None, sha256_crypt__max_rounds=df<<1) self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=%d$nacl$%s' % (df, STUB)) # fallback default rounds - use handler's, but clipped to max rounds c2 = cc.copy(sha256_crypt__default_rounds=None, sha256_crypt__max_rounds=3000) self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=3000$nacl$' + STUB) # TODO: test default falls back to mx / mn if handler has no default. # default rounds - out of bounds self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=1999) cc.copy(sha256_crypt__default_rounds=2000) cc.copy(sha256_crypt__default_rounds=3000) self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=3001) #-------------------------------------------------- # border cases #-------------------------------------------------- # invalid min/max bounds c2 = CryptContext(schemes=["sha256_crypt"]) # NOTE: as of v1.7, these are clipped w/ a warning instead... # self.assertRaises(ValueError, c2.copy, sha256_crypt__min_rounds=-1) # self.assertRaises(ValueError, c2.copy, sha256_crypt__max_rounds=-1) self.assertRaises(ValueError, c2.copy, sha256_crypt__min_rounds=2000, sha256_crypt__max_rounds=1999) # test bad values self.assertRaises(ValueError, CryptContext, sha256_crypt__min_rounds='x') self.assertRaises(ValueError, CryptContext, sha256_crypt__max_rounds='x') self.assertRaises(ValueError, CryptContext, all__vary_rounds='x') self.assertRaises(ValueError, CryptContext, sha256_crypt__default_rounds='x') # test bad types rejected bad = datetime.datetime.now() # picked cause can't be compared to int self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__min_rounds=bad) self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__max_rounds=bad) self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__vary_rounds=bad) self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__default_rounds=bad) def test_51_linear_vary_rounds(self): """test linear vary rounds""" cc = CryptContext(schemes=["sha256_crypt"], sha256_crypt__min_rounds=1995, sha256_crypt__max_rounds=2005, sha256_crypt__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.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0) self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000) c2 = cc.copy(all__vary_rounds="0%") self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0) self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000) # test absolute c2 = cc.copy(all__vary_rounds=1) self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 1) self.assert_rounds_range(c2, "sha256_crypt", 1999, 2001) c2 = cc.copy(all__vary_rounds=100) self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 100) self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005) # test relative c2 = cc.copy(all__vary_rounds="0.1%") self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0.001) self.assert_rounds_range(c2, "sha256_crypt", 1998, 2002) c2 = cc.copy(all__vary_rounds="100%") self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 1.0) self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005) def test_52_log2_vary_rounds(self): """test log2 vary rounds""" cc = CryptContext(schemes=["bcrypt"], bcrypt__min_rounds=15, bcrypt__max_rounds=25, bcrypt__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.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0) self.assert_rounds_range(c2, "bcrypt", 20, 20) c2 = cc.copy(all__vary_rounds="0%") self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0) self.assert_rounds_range(c2, "bcrypt", 20, 20) # test absolute c2 = cc.copy(all__vary_rounds=1) self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 1) self.assert_rounds_range(c2, "bcrypt", 19, 21) c2 = cc.copy(all__vary_rounds=100) self.assertEqual(c2._get_record("bcrypt", None).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.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.01) self.assert_rounds_range(c2, "bcrypt", 20, 20) c2 = cc.copy(all__vary_rounds="49%") self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.49) self.assert_rounds_range(c2, "bcrypt", 20, 20) c2 = cc.copy(all__vary_rounds="50%") self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.5) self.assert_rounds_range(c2, "bcrypt", 19, 20) c2 = cc.copy(all__vary_rounds="100%") self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 1.0) 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:") #=================================================================== # harden_verify / min_verify_time #=================================================================== def test_harden_verify_parsing(self): """harden_verify -- parsing""" warnings.filterwarnings("ignore", ".*harden_verify.*", category=DeprecationWarning) # valid values ctx = CryptContext(schemes=["sha256_crypt"]) self.assertEqual(ctx.harden_verify, None) self.assertEqual(ctx.using(harden_verify="").harden_verify, None) self.assertEqual(ctx.using(harden_verify="true").harden_verify, None) self.assertEqual(ctx.using(harden_verify="false").harden_verify, None) def test_dummy_verify(self): """ dummy_verify() method """ # check dummy_verify() takes expected time expected = 0.05 accuracy = 0.2 handler = DelayHash.using() handler.delay = expected ctx = CryptContext(schemes=[handler]) ctx.dummy_verify() # prime the memoized helpers elapsed, _ = time_call(ctx.dummy_verify) self.assertAlmostEqual(elapsed, expected, delta=expected * accuracy) # TODO: test dummy_verify() invoked by .verify() when hash is None, # and same for .verify_and_update() #=================================================================== # feature tests #=================================================================== def test_61_autodeprecate(self): """test deprecated='auto' is handled correctly""" def getstate(ctx, category=None): return [ctx.handler(scheme, category).deprecated 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") def test_disabled_hashes(self): """disabled hash support""" # # init ref info # from passlib.hash import md5_crypt, unix_disabled ctx = CryptContext(["des_crypt"]) ctx2 = CryptContext(["des_crypt", "unix_disabled"]) h_ref = ctx.hash("foo") h_other = md5_crypt.hash('foo') # # ctx.disable() # # test w/o disabled hash support self.assertRaisesRegex(RuntimeError, "no disabled hasher present", ctx.disable) self.assertRaisesRegex(RuntimeError, "no disabled hasher present", ctx.disable, h_ref) self.assertRaisesRegex(RuntimeError, "no disabled hasher present", ctx.disable, h_other) # test w/ disabled hash support h_dis = ctx2.disable() self.assertEqual(h_dis, unix_disabled.default_marker) h_dis_ref = ctx2.disable(h_ref) self.assertEqual(h_dis_ref, unix_disabled.default_marker + h_ref) h_dis_other = ctx2.disable(h_other) self.assertEqual(h_dis_other, unix_disabled.default_marker + h_other) # don't double-wrap existing disabled hash self.assertEqual(ctx2.disable(h_dis_ref), h_dis_ref) # # ctx.is_enabled() # # test w/o disabled hash support self.assertTrue(ctx.is_enabled(h_ref)) HASH_NOT_IDENTIFIED = "hash could not be identified" self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.is_enabled, h_other) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.is_enabled, h_dis) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.is_enabled, h_dis_ref) # test w/ disabled hash support self.assertTrue(ctx2.is_enabled(h_ref)) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.is_enabled, h_other) self.assertFalse(ctx2.is_enabled(h_dis)) self.assertFalse(ctx2.is_enabled(h_dis_ref)) # # ctx.enable() # # test w/o disabled hash support self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.enable, "") self.assertRaises(TypeError, ctx.enable, None) self.assertEqual(ctx.enable(h_ref), h_ref) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.enable, h_other) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.enable, h_dis) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.enable, h_dis_ref) # test w/ disabled hash support self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx.enable, "") self.assertRaises(TypeError, ctx2.enable, None) self.assertEqual(ctx2.enable(h_ref), h_ref) self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED, ctx2.enable, h_other) self.assertRaisesRegex(ValueError, "cannot restore original hash", ctx2.enable, h_dis) self.assertEqual(ctx2.enable(h_dis_ref), h_ref) #=================================================================== # eoc #=================================================================== import hashlib, time class DelayHash(uh.StaticHandler): """dummy hasher which delays by specified amount""" name = "delay_hash" checksum_chars = uh.LOWER_HEX_CHARS checksum_size = 40 delay = 0 _hash_prefix = u("$x$") def _calc_checksum(self, secret): time.sleep(self.delay) if isinstance(secret, unicode): secret = secret.encode("utf-8") return str_to_uascii(hashlib.sha1(b"prefix" + secret).hexdigest()) #============================================================================= # 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.handler("des_crypt").deprecated) 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.handler("des_crypt").deprecated) self.assertTrue(has_crypt_handler("dummy_2", True)) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/hash.py0000644000175000017500000000717613043457203017251 0ustar biscuitbiscuit00000000000000""" passlib.hash - proxy object mapping hash scheme names -> handlers ================== ***** NOTICE ***** ================== 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`` subpackage. """ #============================================================================= # import proxy object and replace this module #============================================================================= # XXX: if any platform has problem w/ lazy modules, could support 'non-lazy' # version which just imports all schemes known to list_crypt_handlers() from passlib.registry import _proxy import sys sys.modules[__name__] = _proxy #============================================================================= # HACK: the following bit of code is unreachable, but it's presence seems to # help make autocomplete work for certain IDEs such as PyCharm. # this list is automatically regenerated using $SOURCE/admin/regen.py #============================================================================= #---------------------------------------------------- # begin autocomplete hack (autogenerated 2016-11-10) #---------------------------------------------------- if False: from passlib.handlers.argon2 import argon2 from passlib.handlers.bcrypt import bcrypt, bcrypt_sha256 from passlib.handlers.cisco import cisco_asa, cisco_pix, cisco_type7 from passlib.handlers.des_crypt import bigcrypt, bsdi_crypt, crypt16, des_crypt from passlib.handlers.digests import hex_md4, hex_md5, hex_sha1, hex_sha256, hex_sha512, htdigest from passlib.handlers.django import django_bcrypt, django_bcrypt_sha256, django_des_crypt, django_disabled, django_pbkdf2_sha1, django_pbkdf2_sha256, django_salted_md5, django_salted_sha1 from passlib.handlers.fshp import fshp from passlib.handlers.ldap_digests import ldap_bcrypt, ldap_bsdi_crypt, ldap_des_crypt, ldap_md5, ldap_md5_crypt, ldap_plaintext, ldap_salted_md5, ldap_salted_sha1, ldap_sha1, ldap_sha1_crypt, ldap_sha256_crypt, ldap_sha512_crypt from passlib.handlers.md5_crypt import apr_md5_crypt, md5_crypt from passlib.handlers.misc import plaintext, unix_disabled, unix_fallback from passlib.handlers.mssql import mssql2000, mssql2005 from passlib.handlers.mysql import mysql323, mysql41 from passlib.handlers.oracle import oracle10, oracle11 from passlib.handlers.pbkdf2 import atlassian_pbkdf2_sha1, cta_pbkdf2_sha1, dlitz_pbkdf2_sha1, grub_pbkdf2_sha512, ldap_pbkdf2_sha1, ldap_pbkdf2_sha256, ldap_pbkdf2_sha512, pbkdf2_sha1, pbkdf2_sha256, pbkdf2_sha512 from passlib.handlers.phpass import phpass from passlib.handlers.postgres import postgres_md5 from passlib.handlers.roundup import ldap_hex_md5, ldap_hex_sha1, roundup_plaintext from passlib.handlers.scram import scram from passlib.handlers.scrypt import scrypt from passlib.handlers.sha1_crypt import sha1_crypt from passlib.handlers.sha2_crypt import sha256_crypt, sha512_crypt from passlib.handlers.sun_md5_crypt import sun_md5_crypt from passlib.handlers.windows import bsd_nthash, lmhash, msdcc, msdcc2, nthash #---------------------------------------------------- # end autocomplete hack #---------------------------------------------------- #============================================================================= # eoc #============================================================================= passlib-1.7.1/passlib/registry.py0000644000175000017500000004714613043410435020173 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 import exc from passlib.exc import ExpectedTypeError, PasslibWarning from passlib.ifc import PasswordHash from passlib.utils import ( is_crypt_handler, has_crypt as os_crypt_present, unix_crypt_schemes as os_crypt_schemes, ) from passlib.utils.compat import unicode_or_str from passlib.utils.decor import memoize_single_value # 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", argon2 = "passlib.handlers.argon2", 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_asa = "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_argon2 = "passlib.handlers.django", 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", scrypt = "passlib.handlers.scrypt", 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, unicode_or_str), "name must be string 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] #============================================================================= # inspection helpers #============================================================================= #------------------------------------------------------------------ # general #------------------------------------------------------------------ # TODO: needs UTs def _resolve(hasher, param="value"): """ internal helper to resolve argument to hasher object """ if is_crypt_handler(hasher): return hasher elif isinstance(hasher, unicode_or_str): return get_crypt_handler(hasher) else: raise exc.ExpectedTypeError(hasher, unicode_or_str, param) #: backend aliases ANY = "any" BUILTIN = "builtin" OS_CRYPT = "os_crypt" # TODO: needs UTs def has_backend(hasher, backend=ANY, safe=False): """ Test if specified backend is available for hasher. :param hasher: Hasher name or object. :param backend: Name of backend, or ``"any"`` if any backend will do. For hashers without multiple backends, will pretend they have a single backend named ``"builtin"``. :param safe: By default, throws error if backend is unknown. If ``safe=True``, will just return false value. :raises ValueError: * if hasher name is unknown. * if backend is unknown to hasher, and safe=False. :return: True if backend available, False if not available, and None if unknown + safe=True. """ hasher = _resolve(hasher) if backend == ANY: if not hasattr(hasher, "get_backend"): # single backend, assume it's loaded return True # multiple backends, check at least one is loadable try: hasher.get_backend() return True except exc.MissingBackendError: return False # test for specific backend if hasattr(hasher, "has_backend"): # multiple backends if safe and backend not in hasher.backends: return None return hasher.has_backend(backend) # single builtin backend if backend == BUILTIN: return True elif safe: return None else: raise exc.UnknownBackendError(hasher, backend) #------------------------------------------------------------------ # os crypt #------------------------------------------------------------------ # TODO: move unix_crypt_schemes list to here. # os_crypt_schemes -- alias for unix_crypt_schemes above # TODO: needs UTs @memoize_single_value def get_supported_os_crypt_schemes(): """ return tuple of schemes which :func:`crypt.crypt` natively supports. """ if not os_crypt_present: return () cache = tuple(name for name in os_crypt_schemes if get_crypt_handler(name).has_backend(OS_CRYPT)) if not cache: # 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!", exc.PasslibRuntimeWarning) return cache # TODO: needs UTs def has_os_crypt_support(hasher): """ check if hash is supported by native :func:`crypt.crypt` function. if :func:`crypt.crypt` is not present, will always return False. :param hasher: name or hasher object. :returns bool: True if hash format is supported by OS, else False. """ return os_crypt_present and has_backend(hasher, OS_CRYPT, safe=True) #============================================================================= # eof #============================================================================= passlib-1.7.1/passlib/apps.py0000644000175000017500000001535113015205366017263 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", # 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, ) django110_context = LazyCryptContext( schemes=["django_pbkdf2_sha256", "django_pbkdf2_sha1", "django_argon2", "django_bcrypt", "django_bcrypt_sha256", "django_disabled"], ) # this will always point to latest version django_context = django110_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.7.1/passlib/_data/0000755000175000017500000000000013043774617017024 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/_data/wordsets/0000755000175000017500000000000013043774617020676 5ustar biscuitbiscuit00000000000000passlib-1.7.1/passlib/_data/wordsets/eff_short.txt0000644000175000017500000001601413015205366023405 0ustar biscuitbiscuit00000000000000acid acorn acre acts afar affix aged agent agile aging agony ahead aide aids aim ajar alarm alias alibi alien alike alive aloe aloft aloha alone amend amino ample amuse angel anger angle ankle apple april apron aqua area arena argue arise armed armor army aroma array arson art ashen ashes atlas atom attic audio avert avoid awake award awoke axis bacon badge bagel baggy baked baker balmy banjo barge barn bash basil bask batch bath baton bats blade blank blast blaze bleak blend bless blimp blink bloat blob blog blot blunt blurt blush boast boat body boil bok bolt boned boney bonus bony book booth boots boss botch both boxer breed bribe brick bride brim bring brink brisk broad broil broke brook broom brush buck bud buggy bulge bulk bully bunch bunny bunt bush bust busy buzz cable cache cadet cage cake calm cameo canal candy cane canon cape card cargo carol carry carve case cash cause cedar chain chair chant chaos charm chase cheek cheer chef chess chest chew chief chili chill chip chomp chop chow chuck chump chunk churn chute cider cinch city civic civil clad claim clamp clap clash clasp class claw clay clean clear cleat cleft clerk click cling clink clip cloak clock clone cloth cloud clump coach coast coat cod coil coke cola cold colt coma come comic comma cone cope copy coral cork cost cot couch cough cover cozy craft cramp crane crank crate crave crawl crazy creme crepe crept crib cried crisp crook crop cross crowd crown crumb crush crust cub cult cupid cure curl curry curse curve curvy cushy cut cycle dab dad daily dairy daisy dance dandy darn dart dash data date dawn deaf deal dean debit debt debug decaf decal decay deck decor decoy deed delay denim dense dent depth derby desk dial diary dice dig dill dime dimly diner dingy disco dish disk ditch ditzy dizzy dock dodge doing doll dome donor donut dose dot dove down dowry doze drab drama drank draw dress dried drift drill drive drone droop drove drown drum dry duck duct dude dug duke duo dusk dust duty dwarf dwell eagle early earth easel east eaten eats ebay ebony ebook echo edge eel eject elbow elder elf elk elm elope elude elves email emit empty emu enter entry envoy equal erase error erupt essay etch evade even evict evil evoke exact exit fable faced fact fade fall false fancy fang fax feast feed femur fence fend ferry fetal fetch fever fiber fifth fifty film filth final finch fit five flag flaky flame flap flask fled flick fling flint flip flirt float flock flop floss flyer foam foe fog foil folic folk food fool found fox foyer frail frame fray fresh fried frill frisk from front frost froth frown froze fruit gag gains gala game gap gas gave gear gecko geek gem genre gift gig gills given giver glad glass glide gloss glove glow glue goal going golf gong good gooey goofy gore gown grab grain grant grape graph grasp grass grave gravy gray green greet grew grid grief grill grip grit groom grope growl grub grunt guide gulf gulp gummy guru gush gut guy habit half halo halt happy harm hash hasty hatch hate haven hazel hazy heap heat heave hedge hefty help herbs hers hub hug hula hull human humid hump hung hunk hunt hurry hurt hush hut ice icing icon icy igloo image ion iron islam issue item ivory ivy jab jam jaws jazz jeep jelly jet jiffy job jog jolly jolt jot joy judge juice juicy july jumbo jump junky juror jury keep keg kept kick kilt king kite kitty kiwi knee knelt koala kung ladle lady lair lake lance land lapel large lash lasso last latch late lazy left legal lemon lend lens lent level lever lid life lift lilac lily limb limes line lint lion lip list lived liver lunar lunch lung lurch lure lurk lying lyric mace maker malt mama mango manor many map march mardi marry mash match mate math moan mocha moist mold mom moody mop morse most motor motto mount mouse mousy mouth move movie mower mud mug mulch mule mull mumbo mummy mural muse music musky mute nacho nag nail name nanny nap navy near neat neon nerd nest net next niece ninth nutty oak oasis oat ocean oil old olive omen onion only ooze opal open opera opt otter ouch ounce outer oval oven owl ozone pace pagan pager palm panda panic pants panty paper park party pasta patch path patio payer pecan penny pep perch perky perm pest petal petri petty photo plank plant plaza plead plot plow pluck plug plus poach pod poem poet pogo point poise poker polar polio polka polo pond pony poppy pork poser pouch pound pout power prank press print prior prism prize probe prong proof props prude prune pry pug pull pulp pulse puma punch punk pupil puppy purr purse push putt quack quake query quiet quill quilt quit quota quote rabid race rack radar radio raft rage raid rail rake rally ramp ranch range rank rant rash raven reach react ream rebel recap relax relay relic remix repay repel reply rerun reset rhyme rice rich ride rigid rigor rinse riot ripen rise risk ritzy rival river roast robe robin rock rogue roman romp rope rover royal ruby rug ruin rule runny rush rust rut sadly sage said saint salad salon salsa salt same sandy santa satin sauna saved savor sax say scale scam scan scare scarf scary scoff scold scoop scoot scope score scorn scout scowl scrap scrub scuba scuff sect sedan self send sepia serve set seven shack shade shady shaft shaky sham shape share sharp shed sheep sheet shelf shell shine shiny ship shirt shock shop shore shout shove shown showy shred shrug shun shush shut shy sift silk silly silo sip siren sixth size skate skew skid skier skies skip skirt skit sky slab slack slain slam slang slash slate slaw sled sleek sleep sleet slept slice slick slimy sling slip slit slob slot slug slum slurp slush small smash smell smile smirk smog snack snap snare snarl sneak sneer sniff snore snort snout snowy snub snuff speak speed spend spent spew spied spill spiny spoil spoke spoof spool spoon sport spot spout spray spree spur squad squat squid stack staff stage stain stall stamp stand stank stark start stash state stays steam steep stem step stew stick sting stir stock stole stomp stony stood stool stoop stop storm stout stove straw stray strut stuck stud stuff stump stung stunt suds sugar sulk surf sushi swab swan swarm sway swear sweat sweep swell swept swim swing swipe swirl swoop swore syrup tacky taco tag take tall talon tamer tank taper taps tarot tart task taste tasty taunt thank thaw theft theme thigh thing think thong thorn those throb thud thumb thump thus tiara tidal tidy tiger tile tilt tint tiny trace track trade train trait trap trash tray treat tree trek trend trial tribe trick trio trout truce truck trump trunk try tug tulip tummy turf tusk tutor tutu tux tweak tweet twice twine twins twirl twist uncle uncut undo unify union unit untie upon upper urban used user usher utter value vapor vegan venue verse vest veto vice video view viral virus visa visor vixen vocal voice void volt voter vowel wad wafer wager wages wagon wake walk wand wasp watch water wavy wheat whiff whole whoop wick widen widow width wife wifi wilt wimp wind wing wink wipe wired wiry wise wish wispy wok wolf womb wool woozy word work worry wound woven wrath wreck wrist xerox yahoo yam yard year yeast yelp yield yo-yo yodel yoga yoyo yummy zebra zero zesty zippy zone zoom passlib-1.7.1/passlib/_data/wordsets/eff_prefixed.txt0000644000175000017500000002503213015205366024054 0ustar biscuitbiscuit00000000000000aardvark abandoned abbreviate abdomen abhorrence abiding abnormal abrasion absorbing abundant abyss academy accountant acetone achiness acid acoustics acquire acrobat actress acuteness aerosol aesthetic affidavit afloat afraid aftershave again agency aggressor aghast agitate agnostic agonizing agreeing aidless aimlessly ajar alarmclock albatross alchemy alfalfa algae aliens alkaline almanac alongside alphabet already also altitude aluminum always amazingly ambulance amendment amiable ammunition amnesty amoeba amplifier amuser anagram anchor android anesthesia angelfish animal anklet announcer anonymous answer antelope anxiety anyplace aorta apartment apnea apostrophe apple apricot aquamarine arachnid arbitrate ardently arena argument aristocrat armchair aromatic arrowhead arsonist artichoke asbestos ascend aseptic ashamed asinine asleep asocial asparagus astronaut asymmetric atlas atmosphere atom atrocious attic atypical auctioneer auditorium augmented auspicious automobile auxiliary avalanche avenue aviator avocado awareness awhile awkward awning awoke axially azalea babbling backpack badass bagpipe bakery balancing bamboo banana barracuda basket bathrobe bazooka blade blender blimp blouse blurred boatyard bobcat body bogusness bohemian boiler bonnet boots borough bossiness bottle bouquet boxlike breath briefcase broom brushes bubblegum buckle buddhist buffalo bullfrog bunny busboy buzzard cabin cactus cadillac cafeteria cage cahoots cajoling cakewalk calculator camera canister capsule carrot cashew cathedral caucasian caviar ceasefire cedar celery cement census ceramics cesspool chalkboard cheesecake chimney chlorine chopsticks chrome chute cilantro cinnamon circle cityscape civilian clay clergyman clipboard clock clubhouse coathanger cobweb coconut codeword coexistent coffeecake cognitive cohabitate collarbone computer confetti copier cornea cosmetics cotton couch coverless coyote coziness crawfish crewmember crib croissant crumble crystal cubical cucumber cuddly cufflink cuisine culprit cup curry cushion cuticle cybernetic cyclist cylinder cymbal cynicism cypress cytoplasm dachshund daffodil dagger dairy dalmatian dandelion dartboard dastardly datebook daughter dawn daytime dazzler dealer debris decal dedicate deepness defrost degree dehydrator deliverer democrat dentist deodorant depot deranged desktop detergent device dexterity diamond dibs dictionary diffuser digit dilated dimple dinnerware dioxide diploma directory dishcloth ditto dividers dizziness doctor dodge doll dominoes donut doorstep dorsal double downstairs dozed drainpipe dresser driftwood droppings drum dryer dubiously duckling duffel dugout dumpster duplex durable dustpan dutiful duvet dwarfism dwelling dwindling dynamite dyslexia eagerness earlobe easel eavesdrop ebook eccentric echoless eclipse ecosystem ecstasy edged editor educator eelworm eerie effects eggnog egomaniac ejection elastic elbow elderly elephant elfishly eliminator elk elliptical elongated elsewhere elusive elves emancipate embroidery emcee emerald emission emoticon emperor emulate enactment enchilada endorphin energy enforcer engine enhance enigmatic enjoyably enlarged enormous enquirer enrollment ensemble entryway enunciate envoy enzyme epidemic equipment erasable ergonomic erratic eruption escalator eskimo esophagus espresso essay estrogen etching eternal ethics etiquette eucalyptus eulogy euphemism euthanize evacuation evergreen evidence evolution exam excerpt exerciser exfoliate exhale exist exorcist explode exquisite exterior exuberant fabric factory faded failsafe falcon family fanfare fasten faucet favorite feasibly february federal feedback feigned feline femur fence ferret festival fettuccine feudalist feverish fiberglass fictitious fiddle figurine fillet finalist fiscally fixture flashlight fleshiness flight florist flypaper foamless focus foggy folksong fondue footpath fossil fountain fox fragment freeway fridge frosting fruit fryingpan gadget gainfully gallstone gamekeeper gangway garlic gaslight gathering gauntlet gearbox gecko gem generator geographer gerbil gesture getaway geyser ghoulishly gibberish giddiness giftshop gigabyte gimmick giraffe giveaway gizmo glasses gleeful glisten glove glucose glycerin gnarly gnomish goatskin goggles goldfish gong gooey gorgeous gosling gothic gourmet governor grape greyhound grill groundhog grumbling guacamole guerrilla guitar gullible gumdrop gurgling gusto gutless gymnast gynecology gyration habitat hacking haggard haiku halogen hamburger handgun happiness hardhat hastily hatchling haughty hazelnut headband hedgehog hefty heinously helmet hemoglobin henceforth herbs hesitation hexagon hubcap huddling huff hugeness hullabaloo human hunter hurricane hushing hyacinth hybrid hydrant hygienist hypnotist ibuprofen icepack icing iconic identical idiocy idly igloo ignition iguana illuminate imaging imbecile imitator immigrant imprint iodine ionosphere ipad iphone iridescent irksome iron irrigation island isotope issueless italicize itemizer itinerary itunes ivory jabbering jackrabbit jaguar jailhouse jalapeno jamboree janitor jarring jasmine jaundice jawbreaker jaywalker jazz jealous jeep jelly jeopardize jersey jetski jezebel jiffy jigsaw jingling jobholder jockstrap jogging john joinable jokingly journal jovial joystick jubilant judiciary juggle juice jujitsu jukebox jumpiness junkyard juror justifying juvenile kabob kamikaze kangaroo karate kayak keepsake kennel kerosene ketchup khaki kickstand kilogram kimono kingdom kiosk kissing kite kleenex knapsack kneecap knickers koala krypton laboratory ladder lakefront lantern laptop laryngitis lasagna latch laundry lavender laxative lazybones lecturer leftover leggings leisure lemon length leopard leprechaun lettuce leukemia levers lewdness liability library licorice lifeboat lightbulb likewise lilac limousine lint lioness lipstick liquid listless litter liverwurst lizard llama luau lubricant lucidity ludicrous luggage lukewarm lullaby lumberjack lunchbox luridness luscious luxurious lyrics macaroni maestro magazine mahogany maimed majority makeover malformed mammal mango mapmaker marbles massager matchstick maverick maximum mayonnaise moaning mobilize moccasin modify moisture molecule momentum monastery moonshine mortuary mosquito motorcycle mousetrap movie mower mozzarella muckiness mudflow mugshot mule mummy mundane muppet mural mustard mutation myriad myspace myth nail namesake nanosecond napkin narrator nastiness natives nautically navigate nearest nebula nectar nefarious negotiator neither nemesis neoliberal nephew nervously nest netting neuron nevermore nextdoor nicotine niece nimbleness nintendo nirvana nuclear nugget nuisance nullify numbing nuptials nursery nutcracker nylon oasis oat obediently obituary object obliterate obnoxious observer obtain obvious occupation oceanic octopus ocular office oftentimes oiliness ointment older olympics omissible omnivorous oncoming onion onlooker onstage onward onyx oomph opaquely opera opium opossum opponent optical opulently oscillator osmosis ostrich otherwise ought outhouse ovation oven owlish oxford oxidize oxygen oyster ozone pacemaker padlock pageant pajamas palm pamphlet pantyhose paprika parakeet passport patio pauper pavement payphone pebble peculiarly pedometer pegboard pelican penguin peony pepperoni peroxide pesticide petroleum pewter pharmacy pheasant phonebook phrasing physician plank pledge plotted plug plywood pneumonia podiatrist poetic pogo poison poking policeman poncho popcorn porcupine postcard poultry powerboat prairie pretzel princess propeller prune pry pseudo psychopath publisher pucker pueblo pulley pumpkin punchbowl puppy purse pushup putt puzzle pyramid python quarters quesadilla quilt quote racoon radish ragweed railroad rampantly rancidity rarity raspberry ravishing rearrange rebuilt receipt reentry refinery register rehydrate reimburse rejoicing rekindle relic remote renovator reopen reporter request rerun reservoir retriever reunion revolver rewrite rhapsody rhetoric rhino rhubarb rhyme ribbon riches ridden rigidness rimmed riptide riskily ritzy riverboat roamer robe rocket romancer ropelike rotisserie roundtable royal rubber rudderless rugby ruined rulebook rummage running rupture rustproof sabotage sacrifice saddlebag saffron sainthood saltshaker samurai sandworm sapphire sardine sassy satchel sauna savage saxophone scarf scenario schoolbook scientist scooter scrapbook sculpture scythe secretary sedative segregator seismology selected semicolon senator septum sequence serpent sesame settler severely shack shelf shirt shovel shrimp shuttle shyness siamese sibling siesta silicon simmering singles sisterhood sitcom sixfold sizable skateboard skeleton skies skulk skylight slapping sled slingshot sloth slumbering smartphone smelliness smitten smokestack smudge snapshot sneezing sniff snowsuit snugness speakers sphinx spider splashing sponge sprout spur spyglass squirrel statue steamboat stingray stopwatch strawberry student stylus suave subway suction suds suffocate sugar suitcase sulphur superstore surfer sushi swan sweatshirt swimwear sword sycamore syllable symphony synagogue syringes systemize tablespoon taco tadpole taekwondo tagalong takeout tallness tamale tanned tapestry tarantula tastebud tattoo tavern thaw theater thimble thorn throat thumb thwarting tiara tidbit tiebreaker tiger timid tinsel tiptoeing tirade tissue tractor tree tripod trousers trucks tryout tubeless tuesday tugboat tulip tumbleweed tupperware turtle tusk tutorial tuxedo tweezers twins tyrannical ultrasound umbrella umpire unarmored unbuttoned uncle underwear unevenness unflavored ungloved unhinge unicycle unjustly unknown unlocking unmarked unnoticed unopened unpaved unquenched unroll unscrewing untied unusual unveiled unwrinkled unyielding unzip upbeat upcountry update upfront upgrade upholstery upkeep upload uppercut upright upstairs uptown upwind uranium urban urchin urethane urgent urologist username usher utensil utility utmost utopia utterance vacuum vagrancy valuables vanquished vaporizer varied vaseline vegetable vehicle velcro vendor vertebrae vestibule veteran vexingly vicinity videogame viewfinder vigilante village vinegar violin viperfish virus visor vitamins vivacious vixen vocalist vogue voicemail volleyball voucher voyage vulnerable waffle wagon wakeup walrus wanderer wasp water waving wheat whisper wholesaler wick widow wielder wifeless wikipedia wildcat windmill wipeout wired wishbone wizardry wobbliness wolverine womb woolworker workbasket wound wrangle wreckage wristwatch wrongdoing xerox xylophone yacht yahoo yard yearbook yesterday yiddish yield yo-yo yodel yogurt yuppie zealot zebra zeppelin zestfully zigzagged zillion zipping zirconium zodiac zombie zookeeper zucchini passlib-1.7.1/passlib/_data/wordsets/bip39.txt0000644000175000017500000003147513015205366022364 0ustar biscuitbiscuit00000000000000abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve cushion custom cute cycle dad damage damp dance danger daring dash daughter dawn day deal debate debris decade december decide decline decorate decrease deer defense define defy degree delay deliver demand demise denial dentist deny depart depend deposit depth deputy derive describe desert design desk despair destroy detail detect develop device devote diagram dial diamond diary dice diesel diet differ digital dignity dilemma dinner dinosaur direct dirt disagree discover disease dish dismiss disorder display distance divert divide divorce dizzy doctor document dog doll dolphin domain donate donkey donor door dose double dove draft dragon drama drastic draw dream dress drift drill drink drip drive drop drum dry duck dumb dune during dust dutch duty dwarf dynamic eager eagle early earn earth easily east easy echo ecology economy edge edit educate effort egg eight either elbow elder electric elegant element elephant elevator elite else embark embody embrace emerge emotion employ empower empty enable enact end endless endorse enemy energy enforce engage engine enhance enjoy enlist enough enrich enroll ensure enter entire entry envelope episode equal equip era erase erode erosion error erupt escape essay essence estate eternal ethics evidence evil evoke evolve exact example excess exchange excite exclude excuse execute exercise exhaust exhibit exile exist exit exotic expand expect expire explain expose express extend extra eye eyebrow fabric face faculty fade faint faith fall false fame family famous fan fancy fantasy farm fashion fat fatal father fatigue fault favorite feature february federal fee feed feel female fence festival fetch fever few fiber fiction field figure file film filter final find fine finger finish fire firm first fiscal fish fit fitness fix flag flame flash flat flavor flee flight flip float flock floor flower fluid flush fly foam focus fog foil fold follow food foot force forest forget fork fortune forum forward fossil foster found fox fragile frame frequent fresh friend fringe frog front frost frown frozen fruit fuel fun funny furnace fury future gadget gain galaxy gallery game gap garage garbage garden garlic garment gas gasp gate gather gauge gaze general genius genre gentle genuine gesture ghost giant gift giggle ginger giraffe girl give glad glance glare glass glide glimpse globe gloom glory glove glow glue goat goddess gold good goose gorilla gospel gossip govern gown grab grace grain grant grape grass gravity great green grid grief grit grocery group grow grunt guard guess guide guilt guitar gun gym habit hair half hammer hamster hand happy harbor hard harsh harvest hat have hawk hazard head health heart heavy hedgehog height hello helmet help hen hero hidden high hill hint hip hire history hobby hockey hold hole holiday hollow home honey hood hope horn horror horse hospital host hotel hour hover hub huge human humble humor hundred hungry hunt hurdle hurry hurt husband hybrid ice icon idea identify idle ignore ill illegal illness image imitate immense immune impact impose improve impulse inch include income increase index indicate indoor industry infant inflict inform inhale inherit initial inject injury inmate inner innocent input inquiry insane insect inside inspire install intact interest into invest invite involve iron island isolate issue item ivory jacket jaguar jar jazz jealous jeans jelly jewel job join joke journey joy judge juice jump jungle junior junk just kangaroo keen keep ketchup key kick kid kidney kind kingdom kiss kit kitchen kite kitten kiwi knee knife knock know lab label labor ladder lady lake lamp language laptop large later latin laugh laundry lava law lawn lawsuit layer lazy leader leaf learn leave lecture left leg legal legend leisure lemon lend length lens leopard lesson letter level liar liberty library license life lift light like limb limit link lion liquid list little live lizard load loan lobster local lock logic lonely long loop lottery loud lounge love loyal lucky luggage lumber lunar lunch luxury lyrics machine mad magic magnet maid mail main major make mammal man manage mandate mango mansion manual maple marble march margin marine market marriage mask mass master match material math matrix matter maximum maze meadow mean measure meat mechanic medal media melody melt member memory mention menu mercy merge merit merry mesh message metal method middle midnight milk million mimic mind minimum minor minute miracle mirror misery miss mistake mix mixed mixture mobile model modify mom moment monitor monkey monster month moon moral more morning mosquito mother motion motor mountain mouse move movie much muffin mule multiply muscle museum mushroom music must mutual myself mystery myth naive name napkin narrow nasty nation nature near neck need negative neglect neither nephew nerve nest net network neutral never news next nice night noble noise nominee noodle normal north nose notable note nothing notice novel now nuclear number nurse nut oak obey object oblige obscure observe obtain obvious occur ocean october odor off offer office often oil okay old olive olympic omit once one onion online only open opera opinion oppose option orange orbit orchard order ordinary organ orient original orphan ostrich other outdoor outer output outside oval oven over own owner oxygen oyster ozone pact paddle page pair palace palm panda panel panic panther paper parade parent park parrot party pass patch path patient patrol pattern pause pave payment peace peanut pear peasant pelican pen penalty pencil people pepper perfect permit person pet phone photo phrase physical piano picnic picture piece pig pigeon pill pilot pink pioneer pipe pistol pitch pizza place planet plastic plate play please pledge pluck plug plunge poem poet point polar pole police pond pony pool popular portion position possible post potato pottery poverty powder power practice praise predict prefer prepare present pretty prevent price pride primary print priority prison private prize problem process produce profit program project promote proof property prosper protect proud provide public pudding pull pulp pulse pumpkin punch pupil puppy purchase purity purpose purse push put puzzle pyramid quality quantum quarter question quick quit quiz quote rabbit raccoon race rack radar radio rail rain raise rally ramp ranch random range rapid rare rate rather raven raw razor ready real reason rebel rebuild recall receive recipe record recycle reduce reflect reform refuse region regret regular reject relax release relief rely remain remember remind remove render renew rent reopen repair repeat replace report require rescue resemble resist resource response result retire retreat return reunion reveal review reward rhythm rib ribbon rice rich ride ridge rifle right rigid ring riot ripple risk ritual rival river road roast robot robust rocket romance roof rookie room rose rotate rough round route royal rubber rude rug rule run runway rural sad saddle sadness safe sail salad salmon salon salt salute same sample sand satisfy satoshi sauce sausage save say scale scan scare scatter scene scheme school science scissors scorpion scout scrap screen script scrub sea search season seat second secret section security seed seek segment select sell seminar senior sense sentence series service session settle setup seven shadow shaft shallow share shed shell sheriff shield shift shine ship shiver shock shoe shoot shop short shoulder shove shrimp shrug shuffle shy sibling sick side siege sight sign silent silk silly silver similar simple since sing siren sister situate six size skate sketch ski skill skin skirt skull slab slam sleep slender slice slide slight slim slogan slot slow slush small smart smile smoke smooth snack snake snap sniff snow soap soccer social sock soda soft solar soldier solid solution solve someone song soon sorry sort soul sound soup source south space spare spatial spawn speak special speed spell spend sphere spice spider spike spin spirit split spoil sponsor spoon sport spot spray spread spring spy square squeeze squirrel stable stadium staff stage stairs stamp stand start state stay steak steel stem step stereo stick still sting stock stomach stone stool story stove strategy street strike strong struggle student stuff stumble style subject submit subway success such sudden suffer sugar suggest suit summer sun sunny sunset super supply supreme sure surface surge surprise surround survey suspect sustain swallow swamp swap swarm swear sweet swift swim swing switch sword symbol symptom syrup system table tackle tag tail talent talk tank tape target task taste tattoo taxi teach team tell ten tenant tennis tent term test text thank that theme then theory there they thing this thought three thrive throw thumb thunder ticket tide tiger tilt timber time tiny tip tired tissue title toast tobacco today toddler toe together toilet token tomato tomorrow tone tongue tonight tool tooth top topic topple torch tornado tortoise toss total tourist toward tower town toy track trade traffic tragic train transfer trap trash travel tray treat tree trend trial tribe trick trigger trim trip trophy trouble truck true truly trumpet trust truth try tube tuition tumble tuna tunnel turkey turn turtle twelve twenty twice twin twist two type typical ugly umbrella unable unaware uncle uncover under undo unfair unfold unhappy uniform unique unit universe unknown unlock until unusual unveil update upgrade uphold upon upper upset urban urge usage use used useful useless usual utility vacant vacuum vague valid valley valve van vanish vapor various vast vault vehicle velvet vendor venture venue verb verify version very vessel veteran viable vibrant vicious victory video view village vintage violin virtual virus visa visit visual vital vivid vocal voice void volcano volume vote voyage wage wagon wait walk wall walnut want warfare warm warrior wash wasp waste water wave way wealth weapon wear weasel weather web wedding weekend weird welcome west wet whale what wheat wheel when where whip whisper wide width wife wild will win window wine wing wink winner winter wire wisdom wise wish witness wolf woman wonder wood wool word work world worry worth wrap wreck wrestle wrist write wrong yard year yellow you young youth zebra zero zone zoo passlib-1.7.1/passlib/_data/wordsets/eff_long.txt0000644000175000017500000017130013015205366023205 0ustar biscuitbiscuit00000000000000abacus abdomen abdominal abide abiding ability ablaze able abnormal abrasion abrasive abreast abridge abroad abruptly absence absentee absently absinthe absolute absolve abstain abstract absurd accent acclaim acclimate accompany account accuracy accurate accustom acetone achiness aching acid acorn acquaint acquire acre acrobat acronym acting action activate activator active activism activist activity actress acts acutely acuteness aeration aerobics aerosol aerospace afar affair affected affecting affection affidavit affiliate affirm affix afflicted affluent afford affront aflame afloat aflutter afoot afraid afterglow afterlife aftermath aftermost afternoon aged ageless agency agenda agent aggregate aghast agile agility aging agnostic agonize agonizing agony agreeable agreeably agreed agreeing agreement aground ahead ahoy aide aids aim ajar alabaster alarm albatross album alfalfa algebra algorithm alias alibi alienable alienate aliens alike alive alkaline alkalize almanac almighty almost aloe aloft aloha alone alongside aloof alphabet alright although altitude alto aluminum alumni always amaretto amaze amazingly amber ambiance ambiguity ambiguous ambition ambitious ambulance ambush amendable amendment amends amenity amiable amicably amid amigo amino amiss ammonia ammonium amnesty amniotic among amount amperage ample amplifier amplify amply amuck amulet amusable amused amusement amuser amusing anaconda anaerobic anagram anatomist anatomy anchor anchovy ancient android anemia anemic aneurism anew angelfish angelic anger angled angler angles angling angrily angriness anguished angular animal animate animating animation animator anime animosity ankle annex annotate announcer annoying annually annuity anointer another answering antacid antarctic anteater antelope antennae anthem anthill anthology antibody antics antidote antihero antiquely antiques antiquity antirust antitoxic antitrust antiviral antivirus antler antonym antsy anvil anybody anyhow anymore anyone anyplace anything anytime anyway anywhere aorta apache apostle appealing appear appease appeasing appendage appendix appetite appetizer applaud applause apple appliance applicant applied apply appointee appraisal appraiser apprehend approach approval approve apricot april apron aptitude aptly aqua aqueduct arbitrary arbitrate ardently area arena arguable arguably argue arise armadillo armband armchair armed armful armhole arming armless armoire armored armory armrest army aroma arose around arousal arrange array arrest arrival arrive arrogance arrogant arson art ascend ascension ascent ascertain ashamed ashen ashes ashy aside askew asleep asparagus aspect aspirate aspire aspirin astonish astound astride astrology astronaut astronomy astute atlantic atlas atom atonable atop atrium atrocious atrophy attach attain attempt attendant attendee attention attentive attest attic attire attitude attractor attribute atypical auction audacious audacity audible audibly audience audio audition augmented august authentic author autism autistic autograph automaker automated automatic autopilot available avalanche avatar avenge avenging avenue average aversion avert aviation aviator avid avoid await awaken award aware awhile awkward awning awoke awry axis babble babbling babied baboon backache backboard backboned backdrop backed backer backfield backfire backhand backing backlands backlash backless backlight backlit backlog backpack backpedal backrest backroom backshift backside backslid backspace backspin backstab backstage backtalk backtrack backup backward backwash backwater backyard bacon bacteria bacterium badass badge badland badly badness baffle baffling bagel bagful baggage bagged baggie bagginess bagging baggy bagpipe baguette baked bakery bakeshop baking balance balancing balcony balmy balsamic bamboo banana banish banister banjo bankable bankbook banked banker banking banknote bankroll banner bannister banshee banter barbecue barbed barbell barber barcode barge bargraph barista baritone barley barmaid barman barn barometer barrack barracuda barrel barrette barricade barrier barstool bartender barterer bash basically basics basil basin basis basket batboy batch bath baton bats battalion battered battering battery batting battle bauble bazooka blabber bladder blade blah blame blaming blanching blandness blank blaspheme blasphemy blast blatancy blatantly blazer blazing bleach bleak bleep blemish blend bless blighted blimp bling blinked blinker blinking blinks blip blissful blitz blizzard bloated bloating blob blog bloomers blooming blooper blot blouse blubber bluff bluish blunderer blunt blurb blurred blurry blurt blush blustery boaster boastful boasting boat bobbed bobbing bobble bobcat bobsled bobtail bodacious body bogged boggle bogus boil bok bolster bolt bonanza bonded bonding bondless boned bonehead boneless bonelike boney bonfire bonnet bonsai bonus bony boogeyman boogieman book boondocks booted booth bootie booting bootlace bootleg boots boozy borax boring borough borrower borrowing boss botanical botanist botany botch both bottle bottling bottom bounce bouncing bouncy bounding boundless bountiful bovine boxcar boxer boxing boxlike boxy breach breath breeches breeching breeder breeding breeze breezy brethren brewery brewing briar bribe brick bride bridged brigade bright brilliant brim bring brink brisket briskly briskness bristle brittle broadband broadcast broaden broadly broadness broadside broadways broiler broiling broken broker bronchial bronco bronze bronzing brook broom brought browbeat brownnose browse browsing bruising brunch brunette brunt brush brussels brute brutishly bubble bubbling bubbly buccaneer bucked bucket buckle buckshot buckskin bucktooth buckwheat buddhism buddhist budding buddy budget buffalo buffed buffer buffing buffoon buggy bulb bulge bulginess bulgur bulk bulldog bulldozer bullfight bullfrog bullhorn bullion bullish bullpen bullring bullseye bullwhip bully bunch bundle bungee bunion bunkbed bunkhouse bunkmate bunny bunt busboy bush busily busload bust busybody buzz cabana cabbage cabbie cabdriver cable caboose cache cackle cacti cactus caddie caddy cadet cadillac cadmium cage cahoots cake calamari calamity calcium calculate calculus caliber calibrate calm caloric calorie calzone camcorder cameo camera camisole camper campfire camping campsite campus canal canary cancel candied candle candy cane canine canister cannabis canned canning cannon cannot canola canon canopener canopy canteen canyon capable capably capacity cape capillary capital capitol capped capricorn capsize capsule caption captivate captive captivity capture caramel carat caravan carbon cardboard carded cardiac cardigan cardinal cardstock carefully caregiver careless caress caretaker cargo caring carless carload carmaker carnage carnation carnival carnivore carol carpenter carpentry carpool carport carried carrot carrousel carry cartel cartload carton cartoon cartridge cartwheel carve carving carwash cascade case cash casing casino casket cassette casually casualty catacomb catalog catalyst catalyze catapult cataract catatonic catcall catchable catcher catching catchy caterer catering catfight catfish cathedral cathouse catlike catnap catnip catsup cattail cattishly cattle catty catwalk caucasian caucus causal causation cause causing cauterize caution cautious cavalier cavalry caviar cavity cedar celery celestial celibacy celibate celtic cement census ceramics ceremony certainly certainty certified certify cesarean cesspool chafe chaffing chain chair chalice challenge chamber chamomile champion chance change channel chant chaos chaperone chaplain chapped chaps chapter character charbroil charcoal charger charging chariot charity charm charred charter charting chase chasing chaste chastise chastity chatroom chatter chatting chatty cheating cheddar cheek cheer cheese cheesy chef chemicals chemist chemo cherisher cherub chess chest chevron chevy chewable chewer chewing chewy chief chihuahua childcare childhood childish childless childlike chili chill chimp chip chirping chirpy chitchat chivalry chive chloride chlorine choice chokehold choking chomp chooser choosing choosy chop chosen chowder chowtime chrome chubby chuck chug chummy chump chunk churn chute cider cilantro cinch cinema cinnamon circle circling circular circulate circus citable citadel citation citizen citric citrus city civic civil clad claim clambake clammy clamor clamp clamshell clang clanking clapped clapper clapping clarify clarinet clarity clash clasp class clatter clause clavicle claw clay clean clear cleat cleaver cleft clench clergyman clerical clerk clever clicker client climate climatic cling clinic clinking clip clique cloak clobber clock clone cloning closable closure clothes clothing cloud clover clubbed clubbing clubhouse clump clumsily clumsy clunky clustered clutch clutter coach coagulant coastal coaster coasting coastland coastline coat coauthor cobalt cobbler cobweb cocoa coconut cod coeditor coerce coexist coffee cofounder cognition cognitive cogwheel coherence coherent cohesive coil coke cola cold coleslaw coliseum collage collapse collar collected collector collide collie collision colonial colonist colonize colony colossal colt coma come comfort comfy comic coming comma commence commend comment commerce commode commodity commodore common commotion commute commuting compacted compacter compactly compactor companion company compare compel compile comply component composed composer composite compost composure compound compress comprised computer computing comrade concave conceal conceded concept concerned concert conch concierge concise conclude concrete concur condense condiment condition condone conducive conductor conduit cone confess confetti confidant confident confider confiding configure confined confining confirm conflict conform confound confront confused confusing confusion congenial congested congrats congress conical conjoined conjure conjuror connected connector consensus consent console consoling consonant constable constant constrain constrict construct consult consumer consuming contact container contempt contend contented contently contents contest context contort contour contrite control contusion convene convent copartner cope copied copier copilot coping copious copper copy coral cork cornball cornbread corncob cornea corned corner cornfield cornflake cornhusk cornmeal cornstalk corny coronary coroner corporal corporate corral correct corridor corrode corroding corrosive corsage corset cortex cosigner cosmetics cosmic cosmos cosponsor cost cottage cotton couch cough could countable countdown counting countless country county courier covenant cover coveted coveting coyness cozily coziness cozy crabbing crabgrass crablike crabmeat cradle cradling crafter craftily craftsman craftwork crafty cramp cranberry crane cranial cranium crank crate crave craving crawfish crawlers crawling crayfish crayon crazed crazily craziness crazy creamed creamer creamlike crease creasing creatable create creation creative creature credible credibly credit creed creme creole crepe crept crescent crested cresting crestless crevice crewless crewman crewmate crib cricket cried crier crimp crimson cringe cringing crinkle crinkly crisped crisping crisply crispness crispy criteria critter croak crock crook croon crop cross crouch crouton crowbar crowd crown crucial crudely crudeness cruelly cruelness cruelty crumb crummiest crummy crumpet crumpled cruncher crunching crunchy crusader crushable crushed crusher crushing crust crux crying cryptic crystal cubbyhole cube cubical cubicle cucumber cuddle cuddly cufflink culinary culminate culpable culprit cultivate cultural culture cupbearer cupcake cupid cupped cupping curable curator curdle cure curfew curing curled curler curliness curling curly curry curse cursive cursor curtain curtly curtsy curvature curve curvy cushy cusp cussed custard custodian custody customary customer customize customs cut cycle cyclic cycling cyclist cylinder cymbal cytoplasm cytoplast dab dad daffodil dagger daily daintily dainty dairy daisy dallying dance dancing dandelion dander dandruff dandy danger dangle dangling daredevil dares daringly darkened darkening darkish darkness darkroom darling darn dart darwinism dash dastardly data datebook dating daughter daunting dawdler dawn daybed daybreak daycare daydream daylight daylong dayroom daytime dazzler dazzling deacon deafening deafness dealer dealing dealmaker dealt dean debatable debate debating debit debrief debtless debtor debug debunk decade decaf decal decathlon decay deceased deceit deceiver deceiving december decency decent deception deceptive decibel decidable decimal decimeter decipher deck declared decline decode decompose decorated decorator decoy decrease decree dedicate dedicator deduce deduct deed deem deepen deeply deepness deface defacing defame default defeat defection defective defendant defender defense defensive deferral deferred defiance defiant defile defiling define definite deflate deflation deflator deflected deflector defog deforest defraud defrost deftly defuse defy degraded degrading degrease degree dehydrate deity dejected delay delegate delegator delete deletion delicacy delicate delicious delighted delirious delirium deliverer delivery delouse delta deluge delusion deluxe demanding demeaning demeanor demise democracy democrat demote demotion demystify denatured deniable denial denim denote dense density dental dentist denture deny deodorant deodorize departed departure depict deplete depletion deplored deploy deport depose depraved depravity deprecate depress deprive depth deputize deputy derail deranged derby derived desecrate deserve deserving designate designed designer designing deskbound desktop deskwork desolate despair despise despite destiny destitute destruct detached detail detection detective detector detention detergent detest detonate detonator detoxify detract deuce devalue deviancy deviant deviate deviation deviator device devious devotedly devotee devotion devourer devouring devoutly dexterity dexterous diabetes diabetic diabolic diagnoses diagnosis diagram dial diameter diaper diaphragm diary dice dicing dictate dictation dictator difficult diffused diffuser diffusion diffusive dig dilation diligence diligent dill dilute dime diminish dimly dimmed dimmer dimness dimple diner dingbat dinghy dinginess dingo dingy dining dinner diocese dioxide diploma dipped dipper dipping directed direction directive directly directory direness dirtiness disabled disagree disallow disarm disarray disaster disband disbelief disburse discard discern discharge disclose discolor discount discourse discover discuss disdain disengage disfigure disgrace dish disinfect disjoin disk dislike disliking dislocate dislodge disloyal dismantle dismay dismiss dismount disobey disorder disown disparate disparity dispatch dispense dispersal dispersed disperser displace display displease disposal dispose disprove dispute disregard disrupt dissuade distance distant distaste distill distinct distort distract distress district distrust ditch ditto ditzy dividable divided dividend dividers dividing divinely diving divinity divisible divisibly division divisive divorcee dizziness dizzy doable docile dock doctrine document dodge dodgy doily doing dole dollar dollhouse dollop dolly dolphin domain domelike domestic dominion dominoes donated donation donator donor donut doodle doorbell doorframe doorknob doorman doormat doornail doorpost doorstep doorstop doorway doozy dork dormitory dorsal dosage dose dotted doubling douche dove down dowry doze drab dragging dragonfly dragonish dragster drainable drainage drained drainer drainpipe dramatic dramatize drank drapery drastic draw dreaded dreadful dreadlock dreamboat dreamily dreamland dreamless dreamlike dreamt dreamy drearily dreary drench dress drew dribble dried drier drift driller drilling drinkable drinking dripping drippy drivable driven driver driveway driving drizzle drizzly drone drool droop drop-down dropbox dropkick droplet dropout dropper drove drown drowsily drudge drum dry dubbed dubiously duchess duckbill ducking duckling ducktail ducky duct dude duffel dugout duh duke duller dullness duly dumping dumpling dumpster duo dupe duplex duplicate duplicity durable durably duration duress during dusk dust dutiful duty duvet dwarf dweeb dwelled dweller dwelling dwindle dwindling dynamic dynamite dynasty dyslexia dyslexic each eagle earache eardrum earflap earful earlobe early earmark earmuff earphone earpiece earplugs earring earshot earthen earthlike earthling earthly earthworm earthy earwig easeful easel easiest easily easiness easing eastbound eastcoast easter eastward eatable eaten eatery eating eats ebay ebony ebook ecard eccentric echo eclair eclipse ecologist ecology economic economist economy ecosphere ecosystem edge edginess edging edgy edition editor educated education educator eel effective effects efficient effort eggbeater egging eggnog eggplant eggshell egomaniac egotism egotistic either eject elaborate elastic elated elbow eldercare elderly eldest electable election elective elephant elevate elevating elevation elevator eleven elf eligible eligibly eliminate elite elitism elixir elk ellipse elliptic elm elongated elope eloquence eloquent elsewhere elude elusive elves email embargo embark embassy embattled embellish ember embezzle emblaze emblem embody embolism emboss embroider emcee emerald emergency emission emit emote emoticon emotion empathic empathy emperor emphases emphasis emphasize emphatic empirical employed employee employer emporium empower emptier emptiness empty emu enable enactment enamel enchanted enchilada encircle enclose enclosure encode encore encounter encourage encroach encrust encrypt endanger endeared endearing ended ending endless endnote endocrine endorphin endorse endowment endpoint endurable endurance enduring energetic energize energy enforced enforcer engaged engaging engine engorge engraved engraver engraving engross engulf enhance enigmatic enjoyable enjoyably enjoyer enjoying enjoyment enlarged enlarging enlighten enlisted enquirer enrage enrich enroll enslave ensnare ensure entail entangled entering entertain enticing entire entitle entity entomb entourage entrap entree entrench entrust entryway entwine enunciate envelope enviable enviably envious envision envoy envy enzyme epic epidemic epidermal epidermis epidural epilepsy epileptic epilogue epiphany episode equal equate equation equator equinox equipment equity equivocal eradicate erasable erased eraser erasure ergonomic errand errant erratic error erupt escalate escalator escapable escapade escapist escargot eskimo esophagus espionage espresso esquire essay essence essential establish estate esteemed estimate estimator estranged estrogen etching eternal eternity ethanol ether ethically ethics euphemism evacuate evacuee evade evaluate evaluator evaporate evasion evasive even everglade evergreen everybody everyday everyone evict evidence evident evil evoke evolution evolve exact exalted example excavate excavator exceeding exception excess exchange excitable exciting exclaim exclude excluding exclusion exclusive excretion excretory excursion excusable excusably excuse exemplary exemplify exemption exerciser exert exes exfoliate exhale exhaust exhume exile existing exit exodus exonerate exorcism exorcist expand expanse expansion expansive expectant expedited expediter expel expend expenses expensive expert expire expiring explain expletive explicit explode exploit explore exploring exponent exporter exposable expose exposure express expulsion exquisite extended extending extent extenuate exterior external extinct extortion extradite extras extrovert extrude extruding exuberant fable fabric fabulous facebook facecloth facedown faceless facelift faceplate faceted facial facility facing facsimile faction factoid factor factsheet factual faculty fade fading failing falcon fall false falsify fame familiar family famine famished fanatic fancied fanciness fancy fanfare fang fanning fantasize fantastic fantasy fascism fastball faster fasting fastness faucet favorable favorably favored favoring favorite fax feast federal fedora feeble feed feel feisty feline felt-tip feminine feminism feminist feminize femur fence fencing fender ferment fernlike ferocious ferocity ferret ferris ferry fervor fester festival festive festivity fetal fetch fever fiber fiction fiddle fiddling fidelity fidgeting fidgety fifteen fifth fiftieth fifty figment figure figurine filing filled filler filling film filter filth filtrate finale finalist finalize finally finance financial finch fineness finer finicky finished finisher finishing finite finless finlike fiscally fit five flaccid flagman flagpole flagship flagstick flagstone flail flakily flaky flame flammable flanked flanking flannels flap flaring flashback flashbulb flashcard flashily flashing flashy flask flatbed flatfoot flatly flatness flatten flattered flatterer flattery flattop flatware flatworm flavored flavorful flavoring flaxseed fled fleshed fleshy flick flier flight flinch fling flint flip flirt float flock flogging flop floral florist floss flounder flyable flyaway flyer flying flyover flypaper foam foe fog foil folic folk follicle follow fondling fondly fondness fondue font food fool footage football footbath footboard footer footgear foothill foothold footing footless footman footnote footpad footpath footprint footrest footsie footsore footwear footwork fossil foster founder founding fountain fox foyer fraction fracture fragile fragility fragment fragrance fragrant frail frame framing frantic fraternal frayed fraying frays freckled freckles freebase freebee freebie freedom freefall freehand freeing freeload freely freemason freeness freestyle freeware freeway freewill freezable freezing freight french frenzied frenzy frequency frequent fresh fretful fretted friction friday fridge fried friend frighten frightful frigidity frigidly frill fringe frisbee frisk fritter frivolous frolic from front frostbite frosted frostily frosting frostlike frosty froth frown frozen fructose frugality frugally fruit frustrate frying gab gaffe gag gainfully gaining gains gala gallantly galleria gallery galley gallon gallows gallstone galore galvanize gambling game gaming gamma gander gangly gangrene gangway gap garage garbage garden gargle garland garlic garment garnet garnish garter gas gatherer gathering gating gauging gauntlet gauze gave gawk gazing gear gecko geek geiger gem gender generic generous genetics genre gentile gentleman gently gents geography geologic geologist geology geometric geometry geranium gerbil geriatric germicide germinate germless germproof gestate gestation gesture getaway getting getup giant gibberish giblet giddily giddiness giddy gift gigabyte gigahertz gigantic giggle giggling giggly gigolo gilled gills gimmick girdle giveaway given giver giving gizmo gizzard glacial glacier glade gladiator gladly glamorous glamour glance glancing glandular glare glaring glass glaucoma glazing gleaming gleeful glider gliding glimmer glimpse glisten glitch glitter glitzy gloater gloating gloomily gloomy glorified glorifier glorify glorious glory gloss glove glowing glowworm glucose glue gluten glutinous glutton gnarly gnat goal goatskin goes goggles going goldfish goldmine goldsmith golf goliath gonad gondola gone gong good gooey goofball goofiness goofy google goon gopher gore gorged gorgeous gory gosling gossip gothic gotten gout gown grab graceful graceless gracious gradation graded grader gradient grading gradually graduate graffiti grafted grafting grain granddad grandkid grandly grandma grandpa grandson granite granny granola grant granular grape graph grapple grappling grasp grass gratified gratify grating gratitude gratuity gravel graveness graves graveyard gravitate gravity gravy gray grazing greasily greedily greedless greedy green greeter greeting grew greyhound grid grief grievance grieving grievous grill grimace grimacing grime griminess grimy grinch grinning grip gristle grit groggily groggy groin groom groove grooving groovy grope ground grouped grout grove grower growing growl grub grudge grudging grueling gruffly grumble grumbling grumbly grumpily grunge grunt guacamole guidable guidance guide guiding guileless guise gulf gullible gully gulp gumball gumdrop gumminess gumming gummy gurgle gurgling guru gush gusto gusty gutless guts gutter guy guzzler gyration habitable habitant habitat habitual hacked hacker hacking hacksaw had haggler haiku half halogen halt halved halves hamburger hamlet hammock hamper hamster hamstring handbag handball handbook handbrake handcart handclap handclasp handcraft handcuff handed handful handgrip handgun handheld handiness handiwork handlebar handled handler handling handmade handoff handpick handprint handrail handsaw handset handsfree handshake handstand handwash handwork handwoven handwrite handyman hangnail hangout hangover hangup hankering hankie hanky haphazard happening happier happiest happily happiness happy harbor hardcopy hardcore hardcover harddisk hardened hardener hardening hardhat hardhead hardiness hardly hardness hardship hardware hardwired hardwood hardy harmful harmless harmonica harmonics harmonize harmony harness harpist harsh harvest hash hassle haste hastily hastiness hasty hatbox hatchback hatchery hatchet hatching hatchling hate hatless hatred haunt haven hazard hazelnut hazily haziness hazing hazy headache headband headboard headcount headdress headed header headfirst headgear heading headlamp headless headlock headphone headpiece headrest headroom headscarf headset headsman headstand headstone headway headwear heap heat heave heavily heaviness heaving hedge hedging heftiness hefty helium helmet helper helpful helping helpless helpline hemlock hemstitch hence henchman henna herald herbal herbicide herbs heritage hermit heroics heroism herring herself hertz hesitancy hesitant hesitate hexagon hexagram hubcap huddle huddling huff hug hula hulk hull human humble humbling humbly humid humiliate humility humming hummus humongous humorist humorless humorous humpback humped humvee hunchback hundredth hunger hungrily hungry hunk hunter hunting huntress huntsman hurdle hurled hurler hurling hurray hurricane hurried hurry hurt husband hush husked huskiness hut hybrid hydrant hydrated hydration hydrogen hydroxide hyperlink hypertext hyphen hypnoses hypnosis hypnotic hypnotism hypnotist hypnotize hypocrisy hypocrite ibuprofen ice iciness icing icky icon icy idealism idealist idealize ideally idealness identical identify identity ideology idiocy idiom idly igloo ignition ignore iguana illicitly illusion illusive image imaginary imagines imaging imbecile imitate imitation immature immerse immersion imminent immobile immodest immorally immortal immovable immovably immunity immunize impaired impale impart impatient impeach impeding impending imperfect imperial impish implant implement implicate implicit implode implosion implosive imply impolite important importer impose imposing impotence impotency impotent impound imprecise imprint imprison impromptu improper improve improving improvise imprudent impulse impulsive impure impurity iodine iodize ion ipad iphone ipod irate irk iron irregular irrigate irritable irritably irritant irritate islamic islamist isolated isolating isolation isotope issue issuing italicize italics item itinerary itunes ivory ivy jab jackal jacket jackknife jackpot jailbird jailbreak jailer jailhouse jalapeno jam janitor january jargon jarring jasmine jaundice jaunt java jawed jawless jawline jaws jaybird jaywalker jazz jeep jeeringly jellied jelly jersey jester jet jiffy jigsaw jimmy jingle jingling jinx jitters jittery job jockey jockstrap jogger jogging john joining jokester jokingly jolliness jolly jolt jot jovial joyfully joylessly joyous joyride joystick jubilance jubilant judge judgingly judicial judiciary judo juggle juggling jugular juice juiciness juicy jujitsu jukebox july jumble jumbo jump junction juncture june junior juniper junkie junkman junkyard jurist juror jury justice justifier justify justly justness juvenile kabob kangaroo karaoke karate karma kebab keenly keenness keep keg kelp kennel kept kerchief kerosene kettle kick kiln kilobyte kilogram kilometer kilowatt kilt kimono kindle kindling kindly kindness kindred kinetic kinfolk king kinship kinsman kinswoman kissable kisser kissing kitchen kite kitten kitty kiwi kleenex knapsack knee knelt knickers knoll koala kooky kosher krypton kudos kung labored laborer laboring laborious labrador ladder ladies ladle ladybug ladylike lagged lagging lagoon lair lake lance landed landfall landfill landing landlady landless landline landlord landmark landmass landmine landowner landscape landside landslide language lankiness lanky lantern lapdog lapel lapped lapping laptop lard large lark lash lasso last latch late lather latitude latrine latter latticed launch launder laundry laurel lavender lavish laxative lazily laziness lazy lecturer left legacy legal legend legged leggings legible legibly legislate lego legroom legume legwarmer legwork lemon lend length lens lent leotard lesser letdown lethargic lethargy letter lettuce level leverage levers levitate levitator liability liable liberty librarian library licking licorice lid life lifter lifting liftoff ligament likely likeness likewise liking lilac lilly lily limb limeade limelight limes limit limping limpness line lingo linguini linguist lining linked linoleum linseed lint lion lip liquefy liqueur liquid lisp list litigate litigator litmus litter little livable lived lively liver livestock lividly living lizard lubricant lubricate lucid luckily luckiness luckless lucrative ludicrous lugged lukewarm lullaby lumber luminance luminous lumpiness lumping lumpish lunacy lunar lunchbox luncheon lunchroom lunchtime lung lurch lure luridness lurk lushly lushness luster lustfully lustily lustiness lustrous lusty luxurious luxury lying lyrically lyricism lyricist lyrics macarena macaroni macaw mace machine machinist magazine magenta maggot magical magician magma magnesium magnetic magnetism magnetize magnifier magnify magnitude magnolia mahogany maimed majestic majesty majorette majority makeover maker makeshift making malformed malt mama mammal mammary mammogram manager managing manatee mandarin mandate mandatory mandolin manger mangle mango mangy manhandle manhole manhood manhunt manicotti manicure manifesto manila mankind manlike manliness manly manmade manned mannish manor manpower mantis mantra manual many map marathon marauding marbled marbles marbling march mardi margarine margarita margin marigold marina marine marital maritime marlin marmalade maroon married marrow marry marshland marshy marsupial marvelous marxism mascot masculine mashed mashing massager masses massive mastiff matador matchbook matchbox matcher matching matchless material maternal maternity math mating matriarch matrimony matrix matron matted matter maturely maturing maturity mauve maverick maximize maximum maybe mayday mayflower moaner moaning mobile mobility mobilize mobster mocha mocker mockup modified modify modular modulator module moisten moistness moisture molar molasses mold molecular molecule molehill mollusk mom monastery monday monetary monetize moneybags moneyless moneywise mongoose mongrel monitor monkhood monogamy monogram monologue monopoly monorail monotone monotype monoxide monsieur monsoon monstrous monthly monument moocher moodiness moody mooing moonbeam mooned moonlight moonlike moonlit moonrise moonscape moonshine moonstone moonwalk mop morale morality morally morbidity morbidly morphine morphing morse mortality mortally mortician mortified mortify mortuary mosaic mossy most mothball mothproof motion motivate motivator motive motocross motor motto mountable mountain mounted mounting mourner mournful mouse mousiness moustache mousy mouth movable move movie moving mower mowing much muck mud mug mulberry mulch mule mulled mullets multiple multiply multitask multitude mumble mumbling mumbo mummified mummify mummy mumps munchkin mundane municipal muppet mural murkiness murky murmuring muscular museum mushily mushiness mushroom mushy music musket muskiness musky mustang mustard muster mustiness musty mutable mutate mutation mute mutilated mutilator mutiny mutt mutual muzzle myself myspace mystified mystify myth nacho nag nail name naming nanny nanometer nape napkin napped napping nappy narrow nastily nastiness national native nativity natural nature naturist nautical navigate navigator navy nearby nearest nearly nearness neatly neatness nebula nebulizer nectar negate negation negative neglector negligee negligent negotiate nemeses nemesis neon nephew nerd nervous nervy nest net neurology neuron neurosis neurotic neuter neutron never next nibble nickname nicotine niece nifty nimble nimbly nineteen ninetieth ninja nintendo ninth nuclear nuclei nucleus nugget nullify number numbing numbly numbness numeral numerate numerator numeric numerous nuptials nursery nursing nurture nutcase nutlike nutmeg nutrient nutshell nuttiness nutty nuzzle nylon oaf oak oasis oat obedience obedient obituary object obligate obliged oblivion oblivious oblong obnoxious oboe obscure obscurity observant observer observing obsessed obsession obsessive obsolete obstacle obstinate obstruct obtain obtrusive obtuse obvious occultist occupancy occupant occupier occupy ocean ocelot octagon octane october octopus ogle oil oink ointment okay old olive olympics omega omen ominous omission omit omnivore onboard oncoming ongoing onion online onlooker only onscreen onset onshore onslaught onstage onto onward onyx oops ooze oozy opacity opal open operable operate operating operation operative operator opium opossum opponent oppose opposing opposite oppressed oppressor opt opulently osmosis other otter ouch ought ounce outage outback outbid outboard outbound outbreak outburst outcast outclass outcome outdated outdoors outer outfield outfit outflank outgoing outgrow outhouse outing outlast outlet outline outlook outlying outmatch outmost outnumber outplayed outpost outpour output outrage outrank outreach outright outscore outsell outshine outshoot outsider outskirts outsmart outsource outspoken outtakes outthink outward outweigh outwit oval ovary oven overact overall overarch overbid overbill overbite overblown overboard overbook overbuilt overcast overcoat overcome overcook overcrowd overdraft overdrawn overdress overdrive overdue overeager overeater overexert overfed overfeed overfill overflow overfull overgrown overhand overhang overhaul overhead overhear overheat overhung overjoyed overkill overlabor overlaid overlap overlay overload overlook overlord overlying overnight overpass overpay overplant overplay overpower overprice overrate overreach overreact override overripe overrule overrun overshoot overshot oversight oversized oversleep oversold overspend overstate overstay overstep overstock overstuff oversweet overtake overthrow overtime overtly overtone overture overturn overuse overvalue overview overwrite owl oxford oxidant oxidation oxidize oxidizing oxygen oxymoron oyster ozone paced pacemaker pacific pacifier pacifism pacifist pacify padded padding paddle paddling padlock pagan pager paging pajamas palace palatable palm palpable palpitate paltry pampered pamperer pampers pamphlet panama pancake pancreas panda pandemic pang panhandle panic panning panorama panoramic panther pantomime pantry pants pantyhose paparazzi papaya paper paprika papyrus parabola parachute parade paradox paragraph parakeet paralegal paralyses paralysis paralyze paramedic parameter paramount parasail parasite parasitic parcel parched parchment pardon parish parka parking parkway parlor parmesan parole parrot parsley parsnip partake parted parting partition partly partner partridge party passable passably passage passcode passenger passerby passing passion passive passivism passover passport password pasta pasted pastel pastime pastor pastrami pasture pasty patchwork patchy paternal paternity path patience patient patio patriarch patriot patrol patronage patronize pauper pavement paver pavestone pavilion paving pawing payable payback paycheck payday payee payer paying payment payphone payroll pebble pebbly pecan pectin peculiar peddling pediatric pedicure pedigree pedometer pegboard pelican pellet pelt pelvis penalize penalty pencil pendant pending penholder penknife pennant penniless penny penpal pension pentagon pentagram pep perceive percent perch percolate perennial perfected perfectly perfume periscope perish perjurer perjury perkiness perky perm peroxide perpetual perplexed persecute persevere persuaded persuader pesky peso pessimism pessimist pester pesticide petal petite petition petri petroleum petted petticoat pettiness petty petunia phantom phobia phoenix phonebook phoney phonics phoniness phony phosphate photo phrase phrasing placard placate placidly plank planner plant plasma plaster plastic plated platform plating platinum platonic platter platypus plausible plausibly playable playback player playful playgroup playhouse playing playlist playmaker playmate playoff playpen playroom playset plaything playtime plaza pleading pleat pledge plentiful plenty plethora plexiglas pliable plod plop plot plow ploy pluck plug plunder plunging plural plus plutonium plywood poach pod poem poet pogo pointed pointer pointing pointless pointy poise poison poker poking polar police policy polio polish politely polka polo polyester polygon polygraph polymer poncho pond pony popcorn pope poplar popper poppy popsicle populace popular populate porcupine pork porous porridge portable portal portfolio porthole portion portly portside poser posh posing possible possibly possum postage postal postbox postcard posted poster posting postnasal posture postwar pouch pounce pouncing pound pouring pout powdered powdering powdery power powwow pox praising prance prancing pranker prankish prankster prayer praying preacher preaching preachy preamble precinct precise precision precook precut predator predefine predict preface prefix preflight preformed pregame pregnancy pregnant preheated prelaunch prelaw prelude premiere premises premium prenatal preoccupy preorder prepaid prepay preplan preppy preschool prescribe preseason preset preshow president presoak press presume presuming preteen pretended pretender pretense pretext pretty pretzel prevail prevalent prevent preview previous prewar prewashed prideful pried primal primarily primary primate primer primp princess print prior prism prison prissy pristine privacy private privatize prize proactive probable probably probation probe probing probiotic problem procedure process proclaim procreate procurer prodigal prodigy produce product profane profanity professed professor profile profound profusely progeny prognosis program progress projector prologue prolonged promenade prominent promoter promotion prompter promptly prone prong pronounce pronto proofing proofread proofs propeller properly property proponent proposal propose props prorate protector protegee proton prototype protozoan protract protrude proud provable proved proven provided provider providing province proving provoke provoking provolone prowess prowler prowling proximity proxy prozac prude prudishly prune pruning pry psychic public publisher pucker pueblo pug pull pulmonary pulp pulsate pulse pulverize puma pumice pummel punch punctual punctuate punctured pungent punisher punk pupil puppet puppy purchase pureblood purebred purely pureness purgatory purge purging purifier purify purist puritan purity purple purplish purposely purr purse pursuable pursuant pursuit purveyor pushcart pushchair pusher pushiness pushing pushover pushpin pushup pushy putdown putt puzzle puzzling pyramid pyromania python quack quadrant quail quaintly quake quaking qualified qualifier qualify quality qualm quantum quarrel quarry quartered quarterly quarters quartet quench query quicken quickly quickness quicksand quickstep quiet quill quilt quintet quintuple quirk quit quiver quizzical quotable quotation quote rabid race racing racism rack racoon radar radial radiance radiantly radiated radiation radiator radio radish raffle raft rage ragged raging ragweed raider railcar railing railroad railway raisin rake raking rally ramble rambling ramp ramrod ranch rancidity random ranged ranger ranging ranked ranking ransack ranting rants rare rarity rascal rash rasping ravage raven ravine raving ravioli ravishing reabsorb reach reacquire reaction reactive reactor reaffirm ream reanalyze reappear reapply reappoint reapprove rearrange rearview reason reassign reassure reattach reawake rebalance rebate rebel rebirth reboot reborn rebound rebuff rebuild rebuilt reburial rebuttal recall recant recapture recast recede recent recess recharger recipient recital recite reckless reclaim recliner reclining recluse reclusive recognize recoil recollect recolor reconcile reconfirm reconvene recopy record recount recoup recovery recreate rectal rectangle rectified rectify recycled recycler recycling reemerge reenact reenter reentry reexamine referable referee reference refill refinance refined refinery refining refinish reflected reflector reflex reflux refocus refold reforest reformat reformed reformer reformist refract refrain refreeze refresh refried refueling refund refurbish refurnish refusal refuse refusing refutable refute regain regalia regally reggae regime region register registrar registry regress regretful regroup regular regulate regulator rehab reheat rehire rehydrate reimburse reissue reiterate rejoice rejoicing rejoin rekindle relapse relapsing relatable related relation relative relax relay relearn release relenting reliable reliably reliance reliant relic relieve relieving relight relish relive reload relocate relock reluctant rely remake remark remarry rematch remedial remedy remember reminder remindful remission remix remnant remodeler remold remorse remote removable removal removed remover removing rename renderer rendering rendition renegade renewable renewably renewal renewed renounce renovate renovator rentable rental rented renter reoccupy reoccur reopen reorder repackage repacking repaint repair repave repaying repayment repeal repeated repeater repent rephrase replace replay replica reply reporter repose repossess repost repressed reprimand reprint reprise reproach reprocess reproduce reprogram reps reptile reptilian repugnant repulsion repulsive repurpose reputable reputably request require requisite reroute rerun resale resample rescuer reseal research reselect reseller resemble resend resent reset reshape reshoot reshuffle residence residency resident residual residue resigned resilient resistant resisting resize resolute resolved resonant resonate resort resource respect resubmit result resume resupply resurface resurrect retail retainer retaining retake retaliate retention rethink retinal retired retiree retiring retold retool retorted retouch retrace retract retrain retread retreat retrial retrieval retriever retry return retying retype reunion reunite reusable reuse reveal reveler revenge revenue reverb revered reverence reverend reversal reverse reversing reversion revert revisable revise revision revisit revivable revival reviver reviving revocable revoke revolt revolver revolving reward rewash rewind rewire reword rework rewrap rewrite rhyme ribbon ribcage rice riches richly richness rickety ricotta riddance ridden ride riding rifling rift rigging rigid rigor rimless rimmed rind rink rinse rinsing riot ripcord ripeness ripening ripping ripple rippling riptide rise rising risk risotto ritalin ritzy rival riverbank riverbed riverboat riverside riveter riveting roamer roaming roast robbing robe robin robotics robust rockband rocker rocket rockfish rockiness rocking rocklike rockslide rockstar rocky rogue roman romp rope roping roster rosy rotten rotting rotunda roulette rounding roundish roundness roundup roundworm routine routing rover roving royal rubbed rubber rubbing rubble rubdown ruby ruckus rudder rug ruined rule rumble rumbling rummage rumor runaround rundown runner running runny runt runway rupture rural ruse rush rust rut sabbath sabotage sacrament sacred sacrifice sadden saddlebag saddled saddling sadly sadness safari safeguard safehouse safely safeness saffron saga sage sagging saggy said saint sake salad salami salaried salary saline salon saloon salsa salt salutary salute salvage salvaging salvation same sample sampling sanction sanctity sanctuary sandal sandbag sandbank sandbar sandblast sandbox sanded sandfish sanding sandlot sandpaper sandpit sandstone sandstorm sandworm sandy sanitary sanitizer sank santa sapling sappiness sappy sarcasm sarcastic sardine sash sasquatch sassy satchel satiable satin satirical satisfied satisfy saturate saturday sauciness saucy sauna savage savanna saved savings savior savor saxophone say scabbed scabby scalded scalding scale scaling scallion scallop scalping scam scandal scanner scanning scant scapegoat scarce scarcity scarecrow scared scarf scarily scariness scarring scary scavenger scenic schedule schematic scheme scheming schilling schnapps scholar science scientist scion scoff scolding scone scoop scooter scope scorch scorebook scorecard scored scoreless scorer scoring scorn scorpion scotch scoundrel scoured scouring scouting scouts scowling scrabble scraggly scrambled scrambler scrap scratch scrawny screen scribble scribe scribing scrimmage script scroll scrooge scrounger scrubbed scrubber scruffy scrunch scrutiny scuba scuff sculptor sculpture scurvy scuttle secluded secluding seclusion second secrecy secret sectional sector secular securely security sedan sedate sedation sedative sediment seduce seducing segment seismic seizing seldom selected selection selective selector self seltzer semantic semester semicolon semifinal seminar semisoft semisweet senate senator send senior senorita sensation sensitive sensitize sensually sensuous sepia september septic septum sequel sequence sequester series sermon serotonin serpent serrated serve service serving sesame sessions setback setting settle settling setup sevenfold seventeen seventh seventy severity shabby shack shaded shadily shadiness shading shadow shady shaft shakable shakily shakiness shaking shaky shale shallot shallow shame shampoo shamrock shank shanty shape shaping share sharpener sharper sharpie sharply sharpness shawl sheath shed sheep sheet shelf shell shelter shelve shelving sherry shield shifter shifting shiftless shifty shimmer shimmy shindig shine shingle shininess shining shiny ship shirt shivering shock shone shoplift shopper shopping shoptalk shore shortage shortcake shortcut shorten shorter shorthand shortlist shortly shortness shorts shortwave shorty shout shove showbiz showcase showdown shower showgirl showing showman shown showoff showpiece showplace showroom showy shrank shrapnel shredder shredding shrewdly shriek shrill shrimp shrine shrink shrivel shrouded shrubbery shrubs shrug shrunk shucking shudder shuffle shuffling shun shush shut shy siamese siberian sibling siding sierra siesta sift sighing silenced silencer silent silica silicon silk silliness silly silo silt silver similarly simile simmering simple simplify simply sincere sincerity singer singing single singular sinister sinless sinner sinuous sip siren sister sitcom sitter sitting situated situation sixfold sixteen sixth sixties sixtieth sixtyfold sizable sizably size sizing sizzle sizzling skater skating skedaddle skeletal skeleton skeptic sketch skewed skewer skid skied skier skies skiing skilled skillet skillful skimmed skimmer skimming skimpily skincare skinhead skinless skinning skinny skintight skipper skipping skirmish skirt skittle skydiver skylight skyline skype skyrocket skyward slab slacked slacker slacking slackness slacks slain slam slander slang slapping slapstick slashed slashing slate slather slaw sled sleek sleep sleet sleeve slept sliceable sliced slicer slicing slick slider slideshow sliding slighted slighting slightly slimness slimy slinging slingshot slinky slip slit sliver slobbery slogan sloped sloping sloppily sloppy slot slouching slouchy sludge slug slum slurp slush sly small smartly smartness smasher smashing smashup smell smelting smile smilingly smirk smite smith smitten smock smog smoked smokeless smokiness smoking smoky smolder smooth smother smudge smudgy smuggler smuggling smugly smugness snack snagged snaking snap snare snarl snazzy sneak sneer sneeze sneezing snide sniff snippet snipping snitch snooper snooze snore snoring snorkel snort snout snowbird snowboard snowbound snowcap snowdrift snowdrop snowfall snowfield snowflake snowiness snowless snowman snowplow snowshoe snowstorm snowsuit snowy snub snuff snuggle snugly snugness speak spearfish spearhead spearman spearmint species specimen specked speckled specks spectacle spectator spectrum speculate speech speed spellbind speller spelling spendable spender spending spent spew sphere spherical sphinx spider spied spiffy spill spilt spinach spinal spindle spinner spinning spinout spinster spiny spiral spirited spiritism spirits spiritual splashed splashing splashy splatter spleen splendid splendor splice splicing splinter splotchy splurge spoilage spoiled spoiler spoiling spoils spoken spokesman sponge spongy sponsor spoof spookily spooky spool spoon spore sporting sports sporty spotless spotlight spotted spotter spotting spotty spousal spouse spout sprain sprang sprawl spray spree sprig spring sprinkled sprinkler sprint sprite sprout spruce sprung spry spud spur sputter spyglass squabble squad squall squander squash squatted squatter squatting squeak squealer squealing squeamish squeegee squeeze squeezing squid squiggle squiggly squint squire squirt squishier squishy stability stabilize stable stack stadium staff stage staging stagnant stagnate stainable stained staining stainless stalemate staleness stalling stallion stamina stammer stamp stand stank staple stapling starboard starch stardom stardust starfish stargazer staring stark starless starlet starlight starlit starring starry starship starter starting startle startling startup starved starving stash state static statistic statue stature status statute statutory staunch stays steadfast steadier steadily steadying steam steed steep steerable steering steersman stegosaur stellar stem stench stencil step stereo sterile sterility sterilize sterling sternness sternum stew stick stiffen stiffly stiffness stifle stifling stillness stilt stimulant stimulate stimuli stimulus stinger stingily stinging stingray stingy stinking stinky stipend stipulate stir stitch stock stoic stoke stole stomp stonewall stoneware stonework stoning stony stood stooge stool stoop stoplight stoppable stoppage stopped stopper stopping stopwatch storable storage storeroom storewide storm stout stove stowaway stowing straddle straggler strained strainer straining strangely stranger strangle strategic strategy stratus straw stray streak stream street strength strenuous strep stress stretch strewn stricken strict stride strife strike striking strive striving strobe strode stroller strongbox strongly strongman struck structure strudel struggle strum strung strut stubbed stubble stubbly stubborn stucco stuck student studied studio study stuffed stuffing stuffy stumble stumbling stump stung stunned stunner stunning stunt stupor sturdily sturdy styling stylishly stylist stylized stylus suave subarctic subatomic subdivide subdued subduing subfloor subgroup subheader subject sublease sublet sublevel sublime submarine submerge submersed submitter subpanel subpar subplot subprime subscribe subscript subsector subside subsiding subsidize subsidy subsoil subsonic substance subsystem subtext subtitle subtly subtotal subtract subtype suburb subway subwoofer subzero succulent such suction sudden sudoku suds sufferer suffering suffice suffix suffocate suffrage sugar suggest suing suitable suitably suitcase suitor sulfate sulfide sulfite sulfur sulk sullen sulphate sulphuric sultry superbowl superglue superhero superior superjet superman supermom supernova supervise supper supplier supply support supremacy supreme surcharge surely sureness surface surfacing surfboard surfer surgery surgical surging surname surpass surplus surprise surreal surrender surrogate surround survey survival survive surviving survivor sushi suspect suspend suspense sustained sustainer swab swaddling swagger swampland swan swapping swarm sway swear sweat sweep swell swept swerve swifter swiftly swiftness swimmable swimmer swimming swimsuit swimwear swinger swinging swipe swirl switch swivel swizzle swooned swoop swoosh swore sworn swung sycamore sympathy symphonic symphony symptom synapse syndrome synergy synopses synopsis synthesis synthetic syrup system t-shirt tabasco tabby tableful tables tablet tableware tabloid tackiness tacking tackle tackling tacky taco tactful tactical tactics tactile tactless tadpole taekwondo tag tainted take taking talcum talisman tall talon tamale tameness tamer tamper tank tanned tannery tanning tantrum tapeless tapered tapering tapestry tapioca tapping taps tarantula target tarmac tarnish tarot tartar tartly tartness task tassel taste tastiness tasting tasty tattered tattle tattling tattoo taunt tavern thank that thaw theater theatrics thee theft theme theology theorize thermal thermos thesaurus these thesis thespian thicken thicket thickness thieving thievish thigh thimble thing think thinly thinner thinness thinning thirstily thirsting thirsty thirteen thirty thong thorn those thousand thrash thread threaten threefold thrift thrill thrive thriving throat throbbing throng throttle throwaway throwback thrower throwing thud thumb thumping thursday thus thwarting thyself tiara tibia tidal tidbit tidiness tidings tidy tiger tighten tightly tightness tightrope tightwad tigress tile tiling till tilt timid timing timothy tinderbox tinfoil tingle tingling tingly tinker tinkling tinsel tinsmith tint tinwork tiny tipoff tipped tipper tipping tiptoeing tiptop tiring tissue trace tracing track traction tractor trade trading tradition traffic tragedy trailing trailside train traitor trance tranquil transfer transform translate transpire transport transpose trapdoor trapeze trapezoid trapped trapper trapping traps trash travel traverse travesty tray treachery treading treadmill treason treat treble tree trekker tremble trembling tremor trench trend trespass triage trial triangle tribesman tribunal tribune tributary tribute triceps trickery trickily tricking trickle trickster tricky tricolor tricycle trident tried trifle trifocals trillion trilogy trimester trimmer trimming trimness trinity trio tripod tripping triumph trivial trodden trolling trombone trophy tropical tropics trouble troubling trough trousers trout trowel truce truck truffle trump trunks trustable trustee trustful trusting trustless truth try tubby tubeless tubular tucking tuesday tug tuition tulip tumble tumbling tummy turban turbine turbofan turbojet turbulent turf turkey turmoil turret turtle tusk tutor tutu tux tweak tweed tweet tweezers twelve twentieth twenty twerp twice twiddle twiddling twig twilight twine twins twirl twistable twisted twister twisting twisty twitch twitter tycoon tying tyke udder ultimate ultimatum ultra umbilical umbrella umpire unabashed unable unadorned unadvised unafraid unaired unaligned unaltered unarmored unashamed unaudited unawake unaware unbaked unbalance unbeaten unbend unbent unbiased unbitten unblended unblessed unblock unbolted unbounded unboxed unbraided unbridle unbroken unbuckled unbundle unburned unbutton uncanny uncapped uncaring uncertain unchain unchanged uncharted uncheck uncivil unclad unclaimed unclamped unclasp uncle unclip uncloak unclog unclothed uncoated uncoiled uncolored uncombed uncommon uncooked uncork uncorrupt uncounted uncouple uncouth uncover uncross uncrown uncrushed uncured uncurious uncurled uncut undamaged undated undaunted undead undecided undefined underage underarm undercoat undercook undercut underdog underdone underfed underfeed underfoot undergo undergrad underhand underline underling undermine undermost underpaid underpass underpay underrate undertake undertone undertook undertow underuse underwear underwent underwire undesired undiluted undivided undocked undoing undone undrafted undress undrilled undusted undying unearned unearth unease uneasily uneasy uneatable uneaten unedited unelected unending unengaged unenvied unequal unethical uneven unexpired unexposed unfailing unfair unfasten unfazed unfeeling unfiled unfilled unfitted unfitting unfixable unfixed unflawed unfocused unfold unfounded unframed unfreeze unfrosted unfrozen unfunded unglazed ungloved unglue ungodly ungraded ungreased unguarded unguided unhappily unhappy unharmed unhealthy unheard unhearing unheated unhelpful unhidden unhinge unhitched unholy unhook unicorn unicycle unified unifier uniformed uniformly unify unimpeded uninjured uninstall uninsured uninvited union uniquely unisexual unison unissued unit universal universe unjustly unkempt unkind unknotted unknowing unknown unlaced unlatch unlawful unleaded unlearned unleash unless unleveled unlighted unlikable unlimited unlined unlinked unlisted unlit unlivable unloaded unloader unlocked unlocking unlovable unloved unlovely unloving unluckily unlucky unmade unmanaged unmanned unmapped unmarked unmasked unmasking unmatched unmindful unmixable unmixed unmolded unmoral unmovable unmoved unmoving unnamable unnamed unnatural unneeded unnerve unnerving unnoticed unopened unopposed unpack unpadded unpaid unpainted unpaired unpaved unpeeled unpicked unpiloted unpinned unplanned unplanted unpleased unpledged unplowed unplug unpopular unproven unquote unranked unrated unraveled unreached unread unreal unreeling unrefined unrelated unrented unrest unretired unrevised unrigged unripe unrivaled unroasted unrobed unroll unruffled unruly unrushed unsaddle unsafe unsaid unsalted unsaved unsavory unscathed unscented unscrew unsealed unseated unsecured unseeing unseemly unseen unselect unselfish unsent unsettled unshackle unshaken unshaved unshaven unsheathe unshipped unsightly unsigned unskilled unsliced unsmooth unsnap unsocial unsoiled unsold unsolved unsorted unspoiled unspoken unstable unstaffed unstamped unsteady unsterile unstirred unstitch unstopped unstuck unstuffed unstylish unsubtle unsubtly unsuited unsure unsworn untagged untainted untaken untamed untangled untapped untaxed unthawed unthread untidy untie until untimed untimely untitled untoasted untold untouched untracked untrained untreated untried untrimmed untrue untruth unturned untwist untying unusable unused unusual unvalued unvaried unvarying unveiled unveiling unvented unviable unvisited unvocal unwanted unwarlike unwary unwashed unwatched unweave unwed unwelcome unwell unwieldy unwilling unwind unwired unwitting unwomanly unworldly unworn unworried unworthy unwound unwoven unwrapped unwritten unzip upbeat upchuck upcoming upcountry update upfront upgrade upheaval upheld uphill uphold uplifted uplifting upload upon upper upright uprising upriver uproar uproot upscale upside upstage upstairs upstart upstate upstream upstroke upswing uptake uptight uptown upturned upward upwind uranium urban urchin urethane urgency urgent urging urologist urology usable usage useable used uselessly user usher usual utensil utility utilize utmost utopia utter vacancy vacant vacate vacation vagabond vagrancy vagrantly vaguely vagueness valiant valid valium valley valuables value vanilla vanish vanity vanquish vantage vaporizer variable variably varied variety various varmint varnish varsity varying vascular vaseline vastly vastness veal vegan veggie vehicular velcro velocity velvet vendetta vending vendor veneering vengeful venomous ventricle venture venue venus verbalize verbally verbose verdict verify verse version versus vertebrae vertical vertigo very vessel vest veteran veto vexingly viability viable vibes vice vicinity victory video viewable viewer viewing viewless viewpoint vigorous village villain vindicate vineyard vintage violate violation violator violet violin viper viral virtual virtuous virus visa viscosity viscous viselike visible visibly vision visiting visitor visor vista vitality vitalize vitally vitamins vivacious vividly vividness vixen vocalist vocalize vocally vocation voice voicing void volatile volley voltage volumes voter voting voucher vowed vowel voyage wackiness wad wafer waffle waged wager wages waggle wagon wake waking walk walmart walnut walrus waltz wand wannabe wanted wanting wasabi washable washbasin washboard washbowl washcloth washday washed washer washhouse washing washout washroom washstand washtub wasp wasting watch water waviness waving wavy whacking whacky wham wharf wheat whenever whiff whimsical whinny whiny whisking whoever whole whomever whoopee whooping whoops why wick widely widen widget widow width wieldable wielder wife wifi wikipedia wildcard wildcat wilder wildfire wildfowl wildland wildlife wildly wildness willed willfully willing willow willpower wilt wimp wince wincing wind wing winking winner winnings winter wipe wired wireless wiring wiry wisdom wise wish wisplike wispy wistful wizard wobble wobbling wobbly wok wolf wolverine womanhood womankind womanless womanlike womanly womb woof wooing wool woozy word work worried worrier worrisome worry worsening worshiper worst wound woven wow wrangle wrath wreath wreckage wrecker wrecking wrench wriggle wriggly wrinkle wrinkly wrist writing written wrongdoer wronged wrongful wrongly wrongness wrought xbox xerox yahoo yam yanking yapping yard yarn yeah yearbook yearling yearly yearning yeast yelling yelp yen yesterday yiddish yield yin yippee yo-yo yodel yoga yogurt yonder yoyo yummy zap zealous zebra zen zeppelin zero zestfully zesty zigzagged zipfile zipping zippy zips zit zodiac zombie zone zoning zookeeper zoologist zoology zoom passlib-1.7.1/passlib/totp.py0000644000175000017500000021535013041172730017304 0ustar biscuitbiscuit00000000000000"""passlib.totp -- TOTP / RFC6238 / Google Authenticator utilities.""" #============================================================================= # imports #============================================================================= from __future__ import absolute_import, division, print_function from passlib.utils.compat import PY3 # core import base64 import collections import calendar import json import logging; log = logging.getLogger(__name__) import math import struct import sys import time as _time import re if PY3: from urllib.parse import urlparse, parse_qsl, quote, unquote else: from urllib import quote, unquote from urlparse import urlparse, parse_qsl from warnings import warn # site try: # TOTP encrypted keys only supported if cryptography (https://cryptography.io) is installed from cryptography.hazmat.backends import default_backend as _cg_default_backend import cryptography.hazmat.primitives.ciphers.algorithms import cryptography.hazmat.primitives.ciphers.modes from cryptography.hazmat.primitives import ciphers as _cg_ciphers del cryptography except ImportError: log.debug("can't import 'cryptography' package, totp encryption disabled") _cg_ciphers = _cg_default_backend = None # pkg from passlib import exc from passlib.exc import TokenError, MalformedTokenError, InvalidTokenError, UsedTokenError from passlib.utils import (to_unicode, to_bytes, consteq, getrandbytes, rng, SequenceMixin, xor_bytes, getrandstr) from passlib.utils.binary import BASE64_CHARS, b32encode, b32decode from passlib.utils.compat import (u, unicode, native_string_types, bascii_to_str, int_types, num_types, irange, byte_elem_value, UnicodeIO, suppress_cause) from passlib.utils.decor import hybrid_method, memoized_property from passlib.crypto.digest import lookup_hash, compile_hmac, pbkdf2_hmac from passlib.hash import pbkdf2_sha256 # local __all__ = [ # frontend classes "AppWallet", "TOTP", # errors (defined in passlib.exc, but exposed here for convenience) "TokenError", "MalformedTokenError", "InvalidTokenError", "UsedTokenError", # internal helper classes "TotpToken", "TotpMatch", ] #============================================================================= # HACK: python < 2.7.4's urlparse() won't parse query strings unless the url scheme # is one of the schemes in the urlparse.uses_query list. 2.7 abandoned # this, and parses query if present, regardless of the scheme. # as a workaround for older versions, we add "otpauth" to the known list. # this was fixed by https://bugs.python.org/issue9374, in 2.7.4 release. #============================================================================= if sys.version_info < (2,7,4): from urlparse import uses_query if "otpauth" not in uses_query: uses_query.append("otpauth") log.debug("registered 'otpauth' scheme with urlparse.uses_query") del uses_query #============================================================================= # internal helpers #============================================================================= #----------------------------------------------------------------------------- # token parsing / rendering helpers #----------------------------------------------------------------------------- #: regex used to clean whitespace from tokens & keys _clean_re = re.compile(u(r"\s|[-=]"), re.U) _chunk_sizes = [4,6,5] def _get_group_size(klen): """ helper for group_string() -- calculates optimal size of group for given string size. """ # look for exact divisor for size in _chunk_sizes: if not klen % size: return size # fallback to divisor with largest remainder # (so chunks are as close to even as possible) best = _chunk_sizes[0] rem = 0 for size in _chunk_sizes: if klen % size > rem: best = size rem = klen % size return best def group_string(value, sep="-"): """ reformat string into (roughly) evenly-sized groups, separated by **sep**. useful for making tokens & keys easier to read by humans. """ klen = len(value) size = _get_group_size(klen) return sep.join(value[o:o+size] for o in irange(0, klen, size)) #----------------------------------------------------------------------------- # encoding helpers #----------------------------------------------------------------------------- def _decode_bytes(key, format): """ internal TOTP() helper -- decodes key according to specified format. """ if format == "raw": if not isinstance(key, bytes): raise exc.ExpectedTypeError(key, "bytes", "key") return key # for encoded data, key must be either unicode or ascii-encoded bytes, # and must contain a hex or base32 string. key = to_unicode(key, param="key") key = _clean_re.sub("", key).encode("utf-8") # strip whitespace & hypens if format == "hex" or format == "base16": return base64.b16decode(key.upper()) elif format == "base32": return b32decode(key) # XXX: add base64 support? else: raise ValueError("unknown byte-encoding format: %r" % (format,)) #============================================================================= # OTP management #============================================================================= #: flag for detecting if encrypted totp support is present AES_SUPPORT = bool(_cg_ciphers) #: regex for validating secret tags _tag_re = re.compile("(?i)^[a-z0-9][a-z0-9_.-]*$") class AppWallet(object): """ This class stores application-wide secrets that can be used to encrypt & decrypt TOTP keys for storage. It's mostly an internal detail, applications usually just need to pass ``secrets`` or ``secrets_path`` to :meth:`TOTP.using`. .. seealso:: :ref:`totp-storing-instances` for more details on this workflow. Arguments ========= :param secrets: Dict of application secrets to use when encrypting/decrypting stored TOTP keys. This should include a secret to use when encrypting new keys, but may contain additional older secrets to decrypt existing stored keys. The dict should map tags -> secrets, so that each secret is identified by a unique tag. This tag will be stored along with the encrypted key in order to determine which secret should be used for decryption. Tag should be string that starts with regex range ``[a-z0-9]``, and the remaining characters must be in ``[a-z0-9_.-]``. It is recommended to use something like a incremental counter ("1", "2", ...), an ISO date ("2016-01-01", "2016-05-16", ...), or a timestamp ("19803495", "19813495", ...) when assigning tags. This mapping be provided in three formats: * A python dict mapping tag -> secret * A JSON-formatted string containing the dict * A multiline string with the format ``"tag: value\\ntag: value\\n..."`` (This last format is mainly useful when loading from a text file via **secrets_path**) .. seealso:: :func:`generate_secret` to create a secret with sufficient entropy :param secrets_path: Alternately, callers can specify a separate file where the application-wide secrets are stored, using either of the string formats described in **secrets**. :param default_tag: Specifies which tag in **secrets** should be used as the default for encrypting new keys. If omitted, the tags will be sorted, and the largest tag used as the default. if all tags are numeric, they will be sorted numerically; otherwise they will be sorted alphabetically. this permits tags to be assigned numerically, or e.g. using ``YYYY-MM-DD`` dates. :param encrypt_cost: Optional time-cost factor for key encryption. This value corresponds to log2() of the number of PBKDF2 rounds used. .. warning:: The application secret(s) should be stored in a secure location by your application, and each secret should contain a large amount of entropy (to prevent brute-force attacks if the encrypted keys are leaked). :func:`generate_secret` is provided as a convenience helper to generate a new application secret of suitable size. Best practice is to load these values from a file via **secrets_path**, and then have your application give up permission to read this file once it's running. Public Methods ============== .. autoattribute:: has_secrets .. autoattribute:: default_tag Semi-Private Methods ==================== The following methods are used internally by the :class:`TOTP` class in order to encrypt & decrypt keys using the provided application secrets. They will generally not be publically useful, and may have their API changed periodically. .. automethod:: get_secret .. automethod:: encrypt_key .. automethod:: decrypt_key """ #======================================================================== # instance attrs #======================================================================== #: default salt size for encrypt_key() output salt_size = 12 #: default cost (log2 of pbkdf2 rounds) for encrypt_key() output #: NOTE: this is relatively low, since the majority of the security #: relies on a high entropy secret to pass to AES. encrypt_cost = 14 #: map of secret tag -> secret bytes _secrets = None #: tag for default secret default_tag = None #======================================================================== # init #======================================================================== def __init__(self, secrets=None, default_tag=None, encrypt_cost=None, secrets_path=None): # TODO: allow a lot more things to be customized from here, # e.g. setting default TOTP constructor options. # # init cost # if encrypt_cost is not None: if isinstance(encrypt_cost, native_string_types): encrypt_cost = int(encrypt_cost) assert encrypt_cost >= 0 self.encrypt_cost = encrypt_cost # # init secrets map # # load secrets from file (if needed) if secrets_path is not None: if secrets is not None: raise TypeError("'secrets' and 'secrets_path' are mutually exclusive") secrets = open(secrets_path, "rt").read() # parse & store secrets secrets = self._secrets = self._parse_secrets(secrets) # # init default tag/secret # if secrets: if default_tag is not None: # verify that tag is present in map self.get_secret(default_tag) elif all(tag.isdigit() for tag in secrets): default_tag = max(secrets, key=int) else: default_tag = max(secrets) self.default_tag = default_tag def _parse_secrets(self, source): """ parse 'secrets' parameter :returns: Dict[tag:str, secret:bytes] """ # parse string formats # to make this easy to pass in configuration from a separate file, # 'secrets' can be string using two formats -- json & "tag:value\n" check_type = True if isinstance(source, native_string_types): if source.lstrip().startswith(("[", "{")): # json list / dict source = json.loads(source) elif "\n" in source and ":" in source: # multiline string containing series of "tag: value\n" rows; # empty and "#\n" rows are ignored def iter_pairs(source): for line in source.splitlines(): line = line.strip() if line and not line.startswith("#"): tag, secret = line.split(":", 1) yield tag.strip(), secret.strip() source = iter_pairs(source) check_type = False else: raise ValueError("unrecognized secrets string format") # ensure we have iterable of (tag, value) pairs # XXX: could support lists/iterable, but not yet needed... # if isinstance(source, list) or isinstance(source, collections.Iterator): # pass if source is None: return {} elif isinstance(source, dict): source = source.items() elif check_type: raise TypeError("'secrets' must be mapping, or list of items") # parse into final dict, normalizing contents return dict(self._parse_secret_pair(tag, value) for tag, value in source) def _parse_secret_pair(self, tag, value): if isinstance(tag, native_string_types): pass elif isinstance(tag, int): tag = str(tag) else: raise TypeError("tag must be unicode/string: %r" % (tag,)) if not _tag_re.match(tag): raise ValueError("tag contains invalid characters: %r" % (tag,)) if not isinstance(value, bytes): value = to_bytes(value, param="secret %r" % (tag,)) if not value: raise ValueError("tag contains empty secret: %r" % (tag,)) return tag, value #======================================================================== # accessing secrets #======================================================================== @property def has_secrets(self): """whether at least one application secret is present""" return self.default_tag is not None def get_secret(self, tag): """ resolve a secret tag to the secret (as bytes). throws a KeyError if not found. """ secrets = self._secrets if not secrets: raise KeyError("no application secrets configured") try: return secrets[tag] except KeyError: raise suppress_cause(KeyError("unknown secret tag: %r" % (tag,))) #======================================================================== # encrypted key helpers -- used internally by TOTP #======================================================================== @staticmethod def _cipher_aes_key(value, secret, salt, cost, decrypt=False): """ Internal helper for :meth:`encrypt_key` -- handles lowlevel encryption/decryption. Algorithm details: This function uses PBKDF2-HMAC-SHA256 to generate a 32-byte AES key and a 16-byte IV from the application secret & random salt. It then uses AES-256-CTR to encrypt/decrypt the TOTP key. CTR mode was chosen over CBC because the main attack scenario here is that the attacker has stolen the database, and is trying to decrypt a TOTP key (the plaintext value here). To make it hard for them, we want every password to decrypt to a potentially valid key -- thus need to avoid any authentication or padding oracle attacks. While some random padding construction could be devised to make this work for CBC mode, a stream cipher mode is just plain simpler. OFB/CFB modes would also work here, but seeing as they have malleability and cyclic issues (though remote and barely relevant here), CTR was picked as the best overall choice. """ # make sure backend AES support is available if _cg_ciphers is None: raise RuntimeError("TOTP encryption requires 'cryptography' package " "(https://cryptography.io)") # use pbkdf2 to derive both key (32 bytes) & iv (16 bytes) # NOTE: this requires 2 sha256 blocks to be calculated. keyiv = pbkdf2_hmac("sha256", secret, salt=salt, rounds=(1 << cost), keylen=48) # use AES-256-CTR to encrypt/decrypt input value cipher = _cg_ciphers.Cipher(_cg_ciphers.algorithms.AES(keyiv[:32]), _cg_ciphers.modes.CTR(keyiv[32:]), _cg_default_backend()) ctx = cipher.decryptor() if decrypt else cipher.encryptor() return ctx.update(value) + ctx.finalize() def encrypt_key(self, key): """ Helper used to encrypt TOTP keys for storage. :param key: TOTP key to encrypt, as raw bytes. :returns: dict containing encrypted TOTP key & configuration parameters. this format should be treated as opaque, and potentially subject to change, though it is designed to be easily serialized/deserialized (e.g. via JSON). .. note:: This function requires installation of the external `cryptography `_ package. To give some algorithm details: This function uses AES-256-CTR to encrypt the provided data. It takes the application secret and randomly generated salt, and uses PBKDF2-HMAC-SHA256 to combine them and generate the AES key & IV. """ if not key: raise ValueError("no key provided") salt = getrandbytes(rng, self.salt_size) cost = self.encrypt_cost tag = self.default_tag if not tag: raise TypeError("no application secrets configured, can't encrypt OTP key") ckey = self._cipher_aes_key(key, self.get_secret(tag), salt, cost) # XXX: switch to base64? return dict(v=1, c=cost, t=tag, s=b32encode(salt), k=b32encode(ckey)) def decrypt_key(self, enckey): """ Helper used to decrypt TOTP keys from storage format. Consults configured secrets to decrypt key. :param source: source object, as returned by :meth:`encrypt_key`. :returns: ``(key, needs_recrypt)`` -- **key** will be the decrypted key, as bytes. **needs_recrypt** will be a boolean flag indicating whether encryption cost or default tag is too old, and henace that key needs re-encrypting before storing. .. note:: This function requires installation of the external `cryptography `_ package. """ if not isinstance(enckey, dict): raise TypeError("'enckey' must be dictionary") version = enckey.get("v", None) needs_recrypt = False if version == 1: _cipher_key = self._cipher_aes_key else: raise ValueError("missing / unrecognized 'enckey' version: %r" % (version,)) tag = enckey['t'] cost = enckey['c'] key = _cipher_key( value=b32decode(enckey['k']), secret=self.get_secret(tag), salt=b32decode(enckey['s']), cost=cost, ) if cost != self.encrypt_cost or tag != self.default_tag: needs_recrypt = True return key, needs_recrypt #============================================================================= # eoc #============================================================================= #============================================================================= # TOTP class #============================================================================= #: helper to convert HOTP counter to bytes _pack_uint64 = struct.Struct(">Q").pack #: helper to extract value from HOTP digest _unpack_uint32 = struct.Struct(">I").unpack #: dummy bytes used as temp key for .using() method _DUMMY_KEY = b"\x00" * 16 class TOTP(object): """ Helper for generating and verifying TOTP codes. Given a secret key and set of configuration options, this object offers methods for token generation, token validation, and serialization. It can also be used to track important persistent TOTP state, such as the last counter used. This class accepts the following options (only **key** and **format** may be specified as positional arguments). :arg str key: The secret key to use. By default, should be encoded as a base32 string (see **format** for other encodings). Exactly one of **key** or ``new=True`` must be specified. :arg str format: The encoding used by the **key** parameter. May be one of: ``"base32"`` (base32-encoded string), ``"hex"`` (hexadecimal string), or ``"raw"`` (raw bytes). Defaults to ``"base32"``. :param bool new: If ``True``, a new key will be generated using :class:`random.SystemRandom`. Exactly one ``new=True`` or **key** must be specified. :param str label: Label to associate with this token when generating a URI. Displayed to user by most OTP client applications (e.g. Google Authenticator), and typically has format such as ``"John Smith"`` or ``"jsmith@webservice.example.org"``. Defaults to ``None``. See :meth:`to_uri` for details. :param str issuer: String identifying the token issuer (e.g. the domain name of your service). Used internally by some OTP client applications (e.g. Google Authenticator) to distinguish entries which otherwise have the same label. Optional but strongly recommended if you're rendering to a URI. Defaults to ``None``. See :meth:`to_uri` for details. :param int size: Number of bytes when generating new keys. Defaults to size of hash algorithm (e.g. 20 for SHA1). .. warning:: Overriding the default values for ``digits``, ``period``, or ``alg`` may cause problems with some OTP client programs (such as Google Authenticator), which may have these defaults hardcoded. :param int digits: The number of digits in the generated / accepted tokens. Defaults to ``6``. Must be in range [6 .. 10]. .. rst-class:: inline-title .. caution:: Due to a limitation of the HOTP algorithm, the 10th digit can only take on values 0 .. 2, and thus offers very little extra security. :param str alg: Name of hash algorithm to use. Defaults to ``"sha1"``. ``"sha256"`` and ``"sha512"`` are also accepted, per :rfc:`6238`. :param int period: The time-step period to use, in integer seconds. Defaults to ``30``. .. See the passlib documentation for a full list of attributes & methods. """ #============================================================================= # class attrs #============================================================================= #: minimum number of bytes to allow in key, enforced by passlib. # XXX: see if spec says anything relevant to this. _min_key_size = 10 #: minimum & current serialization version (may be set independently by subclasses) min_json_version = json_version = 1 #: AppWallet that this class will use for encrypting/decrypting keys. #: (can be overwritten via the :meth:`TOTP.using()` constructor) wallet = None #: function to get system time in seconds, as needed by :meth:`generate` and :meth:`verify`. #: defaults to :func:`time.time`, but can be overridden on a per-instance basis. now = _time.time #============================================================================= # instance attrs #============================================================================= #--------------------------------------------------------------------------- # configuration attrs #--------------------------------------------------------------------------- #: [private] secret key as raw :class:`!bytes` #: see .key property for public access. _key = None #: [private] cached copy of encrypted secret, #: so .to_json() doesn't have to re-encrypt on each call. _encrypted_key = None #: [private] cached copy of keyed HMAC function, #: so ._generate() doesn't have to rebuild this each time #: ._find_match() invokes it. _keyed_hmac = None #: number of digits in the generated tokens. digits = 6 #: name of hash algorithm in use (e.g. ``"sha1"``) alg = "sha1" #: default label for :meth:`to_uri` label = None #: default issuer for :meth:`to_uri` issuer = None #: number of seconds per counter step. #: *(TOTP uses an internal time-derived counter which #: increments by 1 every* :attr:`!period` *seconds)*. period = 30 #--------------------------------------------------------------------------- # state attrs #--------------------------------------------------------------------------- #: Flag set by deserialization methods to indicate the object needs to be re-serialized. #: This can be for a number of reasons -- encoded using deprecated format, #: or encrypted using a deprecated key or too few rounds. changed = False #============================================================================= # prototype construction #============================================================================= @classmethod def using(cls, digits=None, alg=None, period=None, issuer=None, wallet=None, now=None, **kwds): """ Dynamically create subtype of :class:`!TOTP` class which has the specified defaults set. :parameters: **digits, alg, period, issuer**: All these options are the same as in the :class:`TOTP` constructor, and the resulting class will use any values you specify here as the default for all TOTP instances it creates. :param wallet: Optional :class:`AppWallet` that will be used for encrypting/decrypting keys. :param secrets, secrets_path, encrypt_cost: If specified, these options will be passed to the :class:`AppWallet` constructor, allowing you to directly specify the secret keys that should be used to encrypt & decrypt stored keys. :returns: subclass of :class:`!TOTP`. This method is useful for creating a TOTP class configured to use your application's secrets for encrypting & decrypting keys, as well as create new keys using it's desired configuration defaults. As an example:: >>> # your application can create a custom class when it initializes >>> from passlib.totp import TOTP, generate_secret >>> TotpFactory = TOTP.using(secrets={"1": generate_secret()}) >>> # subsequent TOTP objects created from this factory >>> # will use the specified secrets to encrypt their keys... >>> totp = TotpFactory.new() >>> totp.to_dict() {'enckey': {'c': 14, 'k': 'H77SYXWORDPGVOQTFRR2HFUB3C45XXI7', 's': 'G5DOQPIHIBUM2OOHHADQ', 't': '1', 'v': 1}, 'type': 'totp', 'v': 1} .. seealso:: :ref:`totp-creation` and :ref:`totp-storing-instances` tutorials for a usage example """ # XXX: could add support for setting default match 'window' and 'reuse' policy # :param now: # Optional callable that should return current time for generator to use. # Default to :func:`time.time`. This optional is generally not needed, # and is mainly present for examples & unit-testing. subcls = type("TOTP", (cls,), {}) def norm_param(attr, value): """ helper which uses constructor to validate parameter value. it returns corresponding attribute, so we use normalized value. """ # NOTE: this creates *subclass* instance, # so normalization takes into account any custom params # already stored. kwds = dict(key=_DUMMY_KEY, format="raw") kwds[attr] = value obj = subcls(**kwds) return getattr(obj, attr) if digits is not None: subcls.digits = norm_param("digits", digits) if alg is not None: subcls.alg = norm_param("alg", alg) if period is not None: subcls.period = norm_param("period", period) # XXX: add default size as configurable parameter? if issuer is not None: subcls.issuer = norm_param("issuer", issuer) if kwds: subcls.wallet = AppWallet(**kwds) if wallet: raise TypeError("'wallet' and 'secrets' keywords are mutually exclusive") elif wallet is not None: if not isinstance(wallet, AppWallet): raise exc.ExpectedTypeError(wallet, AppWallet, "wallet") subcls.wallet = wallet if now is not None: assert isinstance(now(), num_types) and now() >= 0, \ "now() function must return non-negative int/float" subcls.now = staticmethod(now) return subcls #============================================================================= # init #============================================================================= @classmethod def new(cls, **kwds): """ convenience alias for creating new TOTP key, same as ``TOTP(new=True)`` """ return cls(new=True, **kwds) def __init__(self, key=None, format="base32", # keyword only... new=False, digits=None, alg=None, size=None, period=None, label=None, issuer=None, changed=False, **kwds): super(TOTP, self).__init__(**kwds) if changed: self.changed = changed # validate & normalize alg info = lookup_hash(alg or self.alg) self.alg = info.name digest_size = info.digest_size if digest_size < 4: raise RuntimeError("%r hash digest too small" % alg) # parse or generate new key if new: # generate new key if key: raise TypeError("'key' and 'new=True' are mutually exclusive") if size is None: # default to digest size, per RFC 6238 Section 5.1 size = digest_size elif size > digest_size: # not forbidden by spec, but would just be wasted bytes. # maybe just warn about this? raise ValueError("'size' should be less than digest size " "(%d)" % digest_size) self.key = getrandbytes(rng, size) elif not key: raise TypeError("must specify either an existing 'key', or 'new=True'") elif format == "encrypted": # NOTE: this handles decrypting & setting '.key' self.encrypted_key = key elif key: # use existing key, encoded using specified self.key = _decode_bytes(key, format) # enforce min key size if len(self.key) < self._min_key_size: # only making this fatal for new=True, # so that existing (but ridiculously small) keys can still be used. msg = "for security purposes, secret key must be >= %d bytes" % self._min_key_size if new: raise ValueError(msg) else: warn(msg, exc.PasslibSecurityWarning, stacklevel=1) # validate digits if digits is None: digits = self.digits if not isinstance(digits, int_types): raise TypeError("digits must be an integer, not a %r" % type(digits)) if digits < 6 or digits > 10: raise ValueError("digits must in range(6,11)") self.digits = digits # validate label if label: self._check_label(label) self.label = label # validate issuer if issuer: self._check_issuer(issuer) self.issuer = issuer # init period if period is not None: self._check_serial(period, "period", minval=1) self.period = period #============================================================================= # helpers to verify value types & ranges #============================================================================= @staticmethod def _check_serial(value, param, minval=0): """ check that serial value (e.g. 'counter') is non-negative integer """ if not isinstance(value, int_types): raise exc.ExpectedTypeError(value, "int", param) if value < minval: raise ValueError("%s must be >= %d" % (param, minval)) @staticmethod def _check_label(label): """ check that label doesn't contain chars forbidden by KeyURI spec """ if label and ":" in label: raise ValueError("label may not contain ':'") @staticmethod def _check_issuer(issuer): """ check that issuer doesn't contain chars forbidden by KeyURI spec """ if issuer and ":" in issuer: raise ValueError("issuer may not contain ':'") #============================================================================= # key attributes #============================================================================= #------------------------------------------------------------------ # raw key #------------------------------------------------------------------ @property def key(self): """ secret key as raw bytes """ return self._key @key.setter def key(self, value): # set key if not isinstance(value, bytes): raise exc.ExpectedTypeError(value, bytes, "key") self._key = value # clear cached properties derived from key self._encrypted_key = self._keyed_hmac = None #------------------------------------------------------------------ # encrypted key #------------------------------------------------------------------ @property def encrypted_key(self): """ secret key, encrypted using application secret. this match the output of :meth:`AppWallet.encrypt_key`, and should be treated as an opaque json serializable object. """ enckey = self._encrypted_key if enckey is None: wallet = self.wallet if not wallet: raise TypeError("no application secrets present, can't encrypt TOTP key") enckey = self._encrypted_key = wallet.encrypt_key(self.key) return enckey @encrypted_key.setter def encrypted_key(self, value): wallet = self.wallet if not wallet: raise TypeError("no application secrets present, can't decrypt TOTP key") self.key, needs_recrypt = wallet.decrypt_key(value) if needs_recrypt: # mark as changed so it gets re-encrypted & written to db self.changed = True else: # cache encrypted key for re-use self._encrypted_key = value #------------------------------------------------------------------ # pretty-printed / encoded key helpers #------------------------------------------------------------------ @property def hex_key(self): """ secret key encoded as hexadecimal string """ return bascii_to_str(base64.b16encode(self.key)).lower() @property def base32_key(self): """ secret key encoded as base32 string """ return b32encode(self.key) def pretty_key(self, format="base32", sep="-"): """ pretty-print the secret key. This is mainly useful for situations where the user cannot get the qrcode to work, and must enter the key manually into their TOTP client. It tries to format the key in a manner that is easier for humans to read. :param format: format to output secret key. ``"hex"`` and ``"base32"`` are both accepted. :param sep: separator to insert to break up key visually. can be any of ``"-"`` (the default), ``" "``, or ``False`` (no separator). :return: key as native string. Usage example:: >>> t = TOTP('s3jdvb7qd2r7jpxx') >>> t.pretty_key() 'S3JD-VB7Q-D2R7-JPXX' """ if format == "hex" or format == "base16": key = self.hex_key elif format == "base32": key = self.base32_key else: raise ValueError("unknown byte-encoding format: %r" % (format,)) if sep: key = group_string(key, sep) return key #============================================================================= # time & token parsing #============================================================================= @classmethod def normalize_time(cls, time): """ Normalize time value to unix epoch seconds. :arg time: Can be ``None``, :class:`!datetime`, or unix epoch timestamp as :class:`!float` or :class:`!int`. If ``None``, uses current system time. Naive datetimes are treated as UTC. :returns: unix epoch timestamp as :class:`int`. """ if isinstance(time, int_types): return time elif isinstance(time, float): return int(time) elif time is None: return int(cls.now()) elif hasattr(time, "utctimetuple"): # coerce datetime to UTC timestamp # NOTE: utctimetuple() assumes naive datetimes are in UTC # NOTE: we explicitly *don't* want microseconds. return calendar.timegm(time.utctimetuple()) else: raise exc.ExpectedTypeError(time, "int, float, or datetime", "time") def _time_to_counter(self, time): """ convert timestamp to HOTP counter using :attr:`period`. """ return time // self.period def _counter_to_time(self, counter): """ convert HOTP counter to timestamp using :attr:`period`. """ return counter * self.period @hybrid_method def normalize_token(self_or_cls, token): """ Normalize OTP token representation: strips whitespace, converts integers to a zero-padded string, validates token content & number of digits. This is a hybrid method -- it can be called at the class level, as ``TOTP.normalize_token()``, or the instance level as ``TOTP().normalize_token()``. It will normalize to the instance-specific number of :attr:`~TOTP.digits`, or use the class default. :arg token: token as ascii bytes, unicode, or an integer. :raises ValueError: if token has wrong number of digits, or contains non-numeric characters. :returns: token as :class:`!unicode` string, containing only digits 0-9. """ digits = self_or_cls.digits if isinstance(token, int_types): token = u("%0*d") % (digits, token) else: token = to_unicode(token, param="token") token = _clean_re.sub(u(""), token) if not token.isdigit(): raise MalformedTokenError("Token must contain only the digits 0-9") if len(token) != digits: raise MalformedTokenError("Token must have exactly %d digits" % digits) return token #============================================================================= # token generation #============================================================================= # # debug helper # def generate_range(self, size, time=None): # counter = self._time_to_counter(time) - (size + 1) // 2 # end = counter + size # while counter <= end: # token = self._generate(counter) # yield TotpToken(self, token, counter) # counter += 1 def generate(self, time=None): """ Generate token for specified time (uses current time if none specified). :arg time: Can be ``None``, a :class:`!datetime`, or class:`!float` / :class:`!int` unix epoch timestamp. If ``None`` (the default), uses current system time. Naive datetimes are treated as UTC. :returns: A :class:`TotpToken` instance, which can be treated as a sequence of ``(token, expire_time)`` -- see that class for more details. Usage example:: >>> # generate a new token, wrapped in a TotpToken instance... >>> otp = TOTP('s3jdvb7qd2r7jpxx') >>> otp.generate(1419622739) >>> # when you just need the token... >>> otp.generate(1419622739).token '897212' """ time = self.normalize_time(time) counter = self._time_to_counter(time) if counter < 0: raise ValueError("timestamp must be >= 0") token = self._generate(counter) return TotpToken(self, token, counter) def _generate(self, counter): """ base implementation of HOTP token generation algorithm. :arg counter: HOTP counter, as non-negative integer :returns: token as unicode string """ # generate digest assert isinstance(counter, int_types), "counter must be integer" assert counter >= 0, "counter must be non-negative" keyed_hmac = self._keyed_hmac if keyed_hmac is None: keyed_hmac = self._keyed_hmac = compile_hmac(self.alg, self.key) digest = keyed_hmac(_pack_uint64(counter)) digest_size = keyed_hmac.digest_info.digest_size assert len(digest) == digest_size, "digest_size: sanity check failed" # derive 31-bit token value assert digest_size >= 20, "digest_size: sanity check 2 failed" # otherwise 0xF+4 will run off end of hash. offset = byte_elem_value(digest[-1]) & 0xF value = _unpack_uint32(digest[offset:offset+4])[0] & 0x7fffffff # render to decimal string, return last chars # NOTE: the 10'th digit is not as secure, as it can only take on values 0-2, not 0-9, # due to 31-bit mask on int ">I". But some servers / clients use it :| # if 31-bit mask removed (which breaks spec), would only get values 0-4. digits = self.digits assert 0 < digits < 11, "digits: sanity check failed" return (u("%0*d") % (digits, value))[-digits:] #============================================================================= # token verification #============================================================================= @classmethod def verify(cls, token, source, **kwds): r""" Convenience wrapper around :meth:`TOTP.from_source` and :meth:`TOTP.match`. This parses a TOTP key & configuration from the specified source, and tries and match the token. It's designed to parallel the :meth:`passlib.ifc.PasswordHash.verify` method. :param token: Token string to match. :param source: Serialized TOTP key. Can be anything accepted by :meth:`TOTP.from_source`. :param \*\*kwds: All additional keywords passed to :meth:`TOTP.match`. :return: A :class:`TotpMatch` instance, or raises a :exc:`TokenError`. """ return cls.from_source(source).match(token, **kwds) def match(self, token, time=None, window=30, skew=0, last_counter=None): """ Match TOTP token against specified timestamp. Searches within a window before & after the provided time, in order to account for transmission delay and small amounts of skew in the client's clock. :arg token: Token to validate. may be integer or string (whitespace and hyphens are ignored). :param time: Unix epoch timestamp, can be any of :class:`!float`, :class:`!int`, or :class:`!datetime`. if ``None`` (the default), uses current system time. *this should correspond to the time the token was received from the client*. :param int window: How far backward and forward in time to search for a match. Measured in seconds. Defaults to ``30``. Typically only useful if set to multiples of :attr:`period`. :param int skew: Adjust timestamp by specified value, to account for excessive client clock skew. Measured in seconds. Defaults to ``0``. Negative skew (the common case) indicates transmission delay, and/or that the client clock is running behind the server. Positive skew indicates the client clock is running ahead of the server (and by enough that it cancels out any negative skew added by the transmission delay). You should ensure the server clock uses a reliable time source such as NTP, so that only the client clock's inaccuracy needs to be accounted for. This is an advanced parameter that should usually be left at ``0``; The **window** parameter is usually enough to account for any observed transmission delay. :param last_counter: Optional value of last counter value that was successfully used. If specified, verify will never search earlier counters, no matter how large the window is. Useful when client has previously authenticated, and thus should never provide a token older than previously verified value. :raises ~passlib.exc.TokenError: If the token is malformed, fails to match, or has already been used. :returns TotpMatch: Returns a :class:`TotpMatch` instance on successful match. Can be treated as tuple of ``(counter, time)``. Raises error if token is malformed / can't be verified. Usage example:: >>> totp = TOTP('s3jdvb7qd2r7jpxx') >>> # valid token for this time period >>> totp.match('897212', 1419622729) >>> # token from counter step 30 sec ago (within allowed window) >>> totp.match('000492', 1419622729) >>> # invalid token -- token from 60 sec ago (outside of window) >>> totp.match('760389', 1419622729) Traceback: ... InvalidTokenError: Token did not match """ time = self.normalize_time(time) self._check_serial(window, "window") client_time = time + skew if last_counter is None: last_counter = -1 start = max(last_counter, self._time_to_counter(client_time - window)) end = self._time_to_counter(client_time + window) + 1 # XXX: could pass 'expected = _time_to_counter(client_time + TRANSMISSION_DELAY)' # to the _find_match() method, would help if window set to very large value. counter = self._find_match(token, start, end) assert counter >= last_counter, "sanity check failed: counter went backward" if counter == last_counter: raise UsedTokenError(expire_time=(last_counter + 1) * self.period) # NOTE: By returning match tied to