summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2013-01-19 15:33:01 -0500
committerEli Collins <elic@assurancetechnologies.com>2013-01-19 15:33:01 -0500
commit4c08f92f9caa64140e0010eae88179f260a28704 (patch)
tree2df0bc5ac5f976348c10a3778992f052f1cccb45
parentffc6b005e32e4d3a71d24d27d22b3f46f4674a52 (diff)
downloadpasslib-4c08f92f9caa64140e0010eae88179f260a28704.tar.gz
minor documentation updates
* added some backwards-compat & error handling notes to CryptContext, as suggested by Thomas Waldmann (https://code.google.com/p/passlib/issues/detail?id=27#c2) * clarified alg descriptions and internal comments for lmhash, md5_crypt, cisco, et al * deprecated_method() decorator now checks if ".. deprecated::" stanza already present in docstring * hash_needs_update() won't be removed until release 2.0
-rw-r--r--docs/conf.py4
-rw-r--r--docs/lib/passlib.context.rst45
-rw-r--r--docs/lib/passlib.hash.lmhash.rst61
-rw-r--r--docs/lib/passlib.hash.md5_crypt.rst2
-rw-r--r--passlib/context.py80
-rw-r--r--passlib/exc.py11
-rw-r--r--passlib/handlers/bcrypt.py19
-rw-r--r--passlib/handlers/cisco.py14
-rw-r--r--passlib/handlers/md5_crypt.py14
-rw-r--r--passlib/utils/__init__.py3
10 files changed, 172 insertions, 81 deletions
diff --git a/docs/conf.py b/docs/conf.py
index b4c7f16..61f9b5a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -247,7 +247,7 @@ htmlhelp_basename = project + 'Doc'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- (index_doc, project + '.tex', project + ' Documentation',
+ (master_doc, project + '.tex', project + ' Documentation',
author, 'manual'),
]
@@ -281,7 +281,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- (index_doc, project, project + ' Documentation',
+ (master_doc, project, project + ' Documentation',
[author], 1)
]
diff --git a/docs/lib/passlib.context.rst b/docs/lib/passlib.context.rst
index ec17bc5..96a6b69 100644
--- a/docs/lib/passlib.context.rst
+++ b/docs/lib/passlib.context.rst
@@ -41,6 +41,7 @@ The CryptContext Class
* `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
@@ -147,6 +148,9 @@ Options which directly affect the behavior of the CryptContext instance:
``["auto"]``, which will configure the CryptContext instance
to deprecate *all* supported schemes except for the default scheme.
+ .. versionadded:: 1.6
+ Added support for the ``["auto"]`` value.
+
.. seealso:: :ref:`context-migration-example` in the tutorial
.. _context-min-verify-time-option:
@@ -416,6 +420,47 @@ 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 it's 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 <context-default-option>`
+ and :ref:`deprecated <context-deprecated-option>`).
+
+:raises KeyError:
+
+ * If the configuration contains an unknown or forbidden option
+ (e.g. :samp:`{scheme}__salt`).
+
+ * If the :ref:`schemes <context-schemes-option>`,
+ :ref:`default <context-default-option>`, or
+ :ref:`deprecated <context-deprecated-option>` 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 issued a generic :exc:`UserWarning` instead
+ of the more specific :exc:`PasslibConfigWarning`.
+
Other Helpers
=============
.. autoclass:: LazyCryptContext([schemes=None,] \*\*kwds [, onload=None])
diff --git a/docs/lib/passlib.hash.lmhash.rst b/docs/lib/passlib.hash.lmhash.rst
index 04012b9..efc0613 100644
--- a/docs/lib/passlib.hash.lmhash.rst
+++ b/docs/lib/passlib.hash.lmhash.rst
@@ -63,30 +63,31 @@ which encode the 16 byte digest. An example hash (of ``password``) is
The digest is calculated as follows:
-1. First the password should be converted to uppercase, and encoded
- to bytes using the "OEM Codepage" used [#cp]_ by the specific release of
- Windows that the host or target server is running.
+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 as normal
- using the ``us-ascii`` encoding. For passwords with non-ASCII
- characters, this step is fraught with compatibility issues
- and border cases (see `Deviations`_ for details).
+ 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 or NULL padded to 14 bytes, as appropriate.
+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 password in step 2 are used as a key,
+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 4 is repeated using the second 7 bytes of the password from step 2,
+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 hexidecimal.
Security Issues
===============
-Due to this myriad of flaws, high-speed password cracking software
-dedicated to LMHASH exists, and the algorithm should be considered broken:
+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.
@@ -106,8 +107,7 @@ dedicated to LMHASH exists, and the algorithm should be considered broken:
Deviations
==========
Passlib's implementation differs from others in a few ways, all related to
-the handling of non-ASCII characters. Future releases of Passlib may update
-the implementation as new information comes up.
+the handling of non-ASCII characters.
* Unicode Policy:
@@ -118,10 +118,11 @@ the implementation as new information comes up.
of XP), and ``cp866`` (used by many Eastern European editions of XP).
Complicating matters further, some third-party implementations are known
to use encodings such as ``latin-1`` and ``utf-8``, which cause
- the non-ASCII characters to have different hashes entirely.
+ non-ASCII characters to hash in a manner incompatible with the canonical
+ MS Windows implementation.
- Thus the application must decide which encoding to use, if it wants
- to provide support for non-ASCII passwords. Passlib uses ``cp437`` as a
+ Thus if an application wishes to provide support for non-ASCII passwords,
+ it must decide which encoding to use. Passlib uses ``cp437`` as a
default, but this may need to be overridden via
``lmhash.encrypt(secret, encoding="some-other-codec")``.
All known encodings are ``us-ascii``-compatible, so for ASCII passwords,
@@ -129,28 +130,32 @@ the implementation as new information comes up.
* 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 converted to uppercase as normal,
- non-ASCII characters are converted in implementation dependant ways:
+ 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 a codepage-dependant table.
- For the most part these tables appear to agree with the Unicode specification,
+ 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]_).
+ Unicode uppercases U+00B5 -> U+039C, but ``cp437`` leaves it unchanged [#uc]_).
- Most third-party implementations (Passlib included) choose to uppercase
- non-ASCII characters according to the Unicode specification, and then
- encode the password; despite the border cases where the hash would not match
- the official windows hash.
+ 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 -
`<http://www.linuxjournal.com/article/2717>`_.
-.. [#cp] The OEM codepage used by specific Window XP (and earlier releases)
+.. [#cp] The OEM codepage used by specific Window XP (and earlier) releases
can be found at `<http://msdn.microsoft.com/nl-nl/goglobal/cc563921%28en-us%29.aspx>`_.
.. [#uc] Online discussion dealing with upper-case encoding issues -
diff --git a/docs/lib/passlib.hash.md5_crypt.rst b/docs/lib/passlib.hash.md5_crypt.rst
index e10afcc..e730d20 100644
--- a/docs/lib/passlib.hash.md5_crypt.rst
+++ b/docs/lib/passlib.hash.md5_crypt.rst
@@ -161,8 +161,6 @@ 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]_.
- However, not only is this attack still only theoretical, but none of MD5's weaknesses
- have been show to affect MD5-Crypt's security.
* More seriously, it's fixed number of rounds (combined with the availability
of high-throughput MD5 implementations) means this algorithm
diff --git a/passlib/context.py b/passlib/context.py
index 89bfd5f..4f7ec13 100644
--- a/passlib/context.py
+++ b/passlib/context.py
@@ -1653,7 +1653,7 @@ class CryptContext(object):
>>> ctx2.default_scheme()
"md5_crypt"
- .. versionchanged:: 1.6
+ .. versionadded:: 1.6
This method was previously named :meth:`!replace`. That alias
has been deprecated, and will be removed in Passlib 1.8.
@@ -1842,7 +1842,7 @@ class CryptContext(object):
.. note::
- If an error occurs during a :meth:`!load` call, the :class`!CryptContext`
+ 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.
@@ -2293,25 +2293,37 @@ class CryptContext(object):
:type secret: unicode, bytes, or None
:param secret:
- Optionally, the secret associated with the hash.
- This is not required, or in fact useful for any current purpose,
- and can be safely omitted. It's mainly present to allow the
- development of future deprecation checks which might need this information.
+ 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``.
- .. versionchanged:: 1.6
- The *secret* argument was added, and this method was renamed
- from the longer alias ``hash_needs_update``.
+ :raises ValueError:
+ If the hash did not match any of the configured :meth:`schemes`.
+
+ .. versionadded:: 1.6
+ This method was previously named :meth:`hash_needs_update`.
.. seealso:: the :ref:`context-migration-example` example in the tutorial.
"""
record = self._get_or_identify_record(hash, scheme, category)
return record.needs_update(hash, secret)
- @deprecated_method(deprecated="1.6", removed="1.8", replacement="CryptContext.needs_update()")
+ @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`"""
+ """Legacy alias for :meth:`needs_update`.
+
+ .. deprecated:: 1.6
+ This method was renamed to :meth:`!needs_update` in version 1.6.
+ This alias will be removed in version 2.0, and should only
+ be used for compatibility with Passlib 1.3 - 1.5.
+ """
return self.needs_update(hash, scheme, category)
def genconfig(self, scheme=None, category=None, **settings):
@@ -2466,16 +2478,16 @@ class CryptContext(object):
:param \*\*kwds:
All other keyword options are passed to the selected algorithm's
- :meth:`~passlib.ifc.PasswordHash.encrypt` method.
+ :meth:`PasswordHash.encrypt() <passlib.ifc.PasswordHash.encrypt>` method.
:returns:
The secret as encoded by the specified algorithm and options.
The return value will always be a :class:`!str`.
:raises TypeError, ValueError:
- * if any of the arguments have an invalid type or value.
- * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.encrypt`
- method throws an error based on *secret* or the provided *kwds*.
+ * If any of the arguments have an invalid type or value.
+ This includes any keywords passed to the underlying hash's
+ :meth:`PasswordHash.encrypt() <passlib.ifc.PasswordHash.encrypt>` method.
.. seealso:: the :ref:`context-basic-example` example in the tutorial
"""
@@ -2517,14 +2529,19 @@ class CryptContext(object):
and should match it's :attr:`~passlib.ifc.PasswordHash.context_kwds`.
:returns:
- ``True`` if the password matched hash, else ``False``.
+ ``True`` if the password matched the hash, else ``False``.
- :raises TypeError, ValueError:
- * if any of the arguments have an invalid type or value.
- * if the selected algorithm's underlying :meth:`~passlib.ifc.PasswordHash.verify`
- method throws an error based on *secret* or the provided *kwds*.
+ :raises ValueError:
+ * if the hash did not match any of the configured :meth:`schemes`.
- :raises ValueError: if the hash could not be identified.
+ * if any of the arguments have an invalid value (this includes
+ any keywords passed to the underlying hash's
+ :meth:`PasswordHash.verify() <passlib.ifc.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() <passlib.ifc.PasswordHash.verify>` method).
.. seealso:: the :ref:`context-basic-example` example in the tutorial
"""
@@ -2569,20 +2586,25 @@ class CryptContext(object):
:param \*\*kwds:
all additional keywords are passed to the appropriate handler,
- and should match it's :attr:`context keywords <passlib.hash.PasswordHash.context_kwds>`.
+ and should match that hash's
+ :attr:`PasswordHash.context_kwds <passlib.ifc.PasswordHash.context_kwds>`.
:returns:
This function returns a tuple containing two elements:
- the first indicates whether the password verified,
- and the second whether the existing hash needs to be replaced.
- The return value will always match one of the following 3 cases:
+ ``(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 upgrading.
+ and the hash does not need updating.
* ``(True, str)`` indicates the secret verified correctly,
- and the existing hash needs to be updated. the :class:`!str`
- will be the freshly generated hash to replace the old one with.
+ 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.
"""
@@ -2643,6 +2665,8 @@ class LazyCryptContext(CryptContext):
As well, it allows constructing a context at *module-init* time,
but using :func:`!onload()` to provide dynamic configuration
at *application-run* time.
+
+ .. versionadded:: 1.4
"""
_lazy_kwds = None
diff --git a/passlib/exc.py b/passlib/exc.py
index bdcbc5e..8d872a7 100644
--- a/passlib/exc.py
+++ b/passlib/exc.py
@@ -43,7 +43,8 @@ class PasswordSizeError(ValueError):
# warnings
#=============================================================================
class PasslibWarning(UserWarning):
- """base class for Passlib's user warnings.
+ """base class for Passlib's user warnings,
+ derives from the builtin :exc:`UserWarning`.
.. versionadded:: 1.6
"""
@@ -61,6 +62,8 @@ class PasslibConfigWarning(PasslibWarning):
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):
@@ -75,6 +78,8 @@ class PasslibHashWarning(PasslibWarning):
* A malformed hash string was encountered which (while parsable)
should be re-encoded.
+
+ .. versionadded:: 1.6
"""
class PasslibRuntimeWarning(PasslibWarning):
@@ -83,11 +88,15 @@ class PasslibRuntimeWarning(PasslibWarning):
The fact that it's a warning instead of an error means Passlib
was able to correct for the issue, but that it's anonmalous enough
that the developers would love to hear under what conditions it occurred.
+
+ .. versionadded:: 1.6
"""
class PasslibSecurityWarning(PasslibWarning):
"""Special warning issued when Passlib encounters something
that might affect security.
+
+ .. versionadded:: 1.6
"""
#=============================================================================
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index 0e76524..61b0829 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -117,9 +117,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
checksum_chars = bcrypt64.charmap
#--HasManyIdents--
- default_ident = u("$2a$")
- ident_values = (u("$2$"), IDENT_2A, IDENT_2X, IDENT_2Y)
- ident_aliases = {u("2"): u("$2$"), u("2a"): IDENT_2A, u("2y"): IDENT_2Y}
+ default_ident = IDENT_2A
+ ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y)
+ ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y}
#--HasSalt--
min_salt_size = max_salt_size = 22
@@ -128,7 +128,7 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
#--HasRounds--
default_rounds = 12 # current passlib default
- min_rounds = 4 # bcrypt spec specified minimum
+ min_rounds = 4 # minimum from bcrypt specification
max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)
rounds_cost = "log2"
@@ -164,8 +164,13 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
if ident is None:
ident = self.ident
if ident == IDENT_2Y:
+ # none of passlib's backends suffered from crypt_blowfish's
+ # buggy "2a" hash, which means we can safely implement
+ # crypt_blowfish's "2y" hash by passing "2a" to the backends.
ident = IDENT_2A
else:
+ # no backends currently support 2x, but that should have
+ # been caught earlier in from_string()
assert ident != IDENT_2X
config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
return uascii_to_str(config)
@@ -197,7 +202,8 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
return hash
def _generate_salt(self, salt_size):
- # override to correct generate salt bits
+ # generate random salt as normal,
+ # but repair last char so the padding bits always decode to zero.
salt = super(bcrypt, self)._generate_salt(salt_size)
return bcrypt64.repair_unused(salt)
@@ -251,7 +257,8 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
@classproperty
def _has_backend_os_crypt(cls):
- # XXX: what to do if only h2 is supported? h1 is *very* rare.
+ # XXX: what to do if "2" isn't supported, but "2a" is?
+ # "2" is *very* rare, and can fake it using "2a"+repeat_string
h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju'
h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty'
return test_crypt("test",h1) and test_crypt("test", h2)
diff --git a/passlib/handlers/cisco.py b/passlib/handlers/cisco.py
index 4f469fc..b1d25b5 100644
--- a/passlib/handlers/cisco.py
+++ b/passlib/handlers/cisco.py
@@ -61,14 +61,14 @@ class cisco_pix(uh.HasUserContext, uh.StaticHandler):
user = self.user
if user:
- # NOTE: not *positive* about this, but it looks like per-user
- # accounts use first 4 chars of user as salt, whereas global
- # "enable" passwords don't have any salt at all.
+ # not positive about this, but it looks like per-user
+ # accounts use the first 4 chars of the username as the salt,
+ # whereas global "enable" passwords don't have any salt at all.
if isinstance(user, unicode):
user = user.encode("utf-8")
secret += user[:4]
- # pad/truncate to 16
+ # null-pad or truncate to 16 bytes
secret = right_pad_string(secret, 16)
# md5 digest
@@ -123,6 +123,8 @@ class cisco_type7(uh.GenericHandler):
setting_kwds = ("salt",)
checksum_chars = uh.UPPER_HEX_CHARS
+ # 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
@@ -154,9 +156,9 @@ class cisco_type7(uh.GenericHandler):
self.salt = self._norm_salt(salt)
def _norm_salt(self, salt):
- # NOTE: the "salt" for this algorithm is a small integer.
+ "the salt for this algorithm is an integer 0-52, not a string"
# XXX: not entirely sure that values >15 are valid, so for
- # compatibility we don't output those values but we do accept them.
+ # compatibility we don't output those values, but we do accept them.
if salt is None:
if self.use_defaults:
salt = self._generate_salt()
diff --git a/passlib/handlers/md5_crypt.py b/passlib/handlers/md5_crypt.py
index ee49236..642316e 100644
--- a/passlib/handlers/md5_crypt.py
+++ b/passlib/handlers/md5_crypt.py
@@ -60,7 +60,7 @@ def _raw_md5_crypt(pwd, salt, use_apr=False):
# 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.
+ # implementation of $1$ wasn't sufficient. *nothing else* was changed.
#===================================================================
# init & validate inputs
@@ -143,8 +143,8 @@ def _raw_md5_crypt(pwd, salt, use_apr=False):
# 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'; determined
- # by the whether 'i' a multiple of 2,3, and/or 7.
+ # 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)';
@@ -152,12 +152,12 @@ def _raw_md5_crypt(pwd, salt, use_apr=False):
#
# Using these observations, the following code...
# * calculates the round-specific combination of salt & pwd for each round 0-41
- # * runs through as many 42-round blocks as possible
- # * runs through as many pairs of rounds as possible for remaining rounds
- # * performs once last round if the total rounds should be odd.
+ # * 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 40,000+ times in python, resulting in ~20% increase in
+ # 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
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index b528e63..654124e 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -175,7 +175,8 @@ def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True,
warn(text, DeprecationWarning, stacklevel=2)
return func(*args, **kwds)
update_wrapper(wrapper, func)
- if updoc and (deprecated or removed) and wrapper.__doc__:
+ if updoc and (deprecated or removed) and \
+ wrapper.__doc__ and ".. deprecated::" not in wrapper.__doc__:
txt = deprecated or ''
if removed or replacement:
txt += "\n "