summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2016-06-15 17:43:31 -0400
committerEli Collins <elic@assurancetechnologies.com>2016-06-15 17:43:31 -0400
commit713c0bb81f3cec4ee15715657c627a9757a2edf2 (patch)
tree51161d33d6fb5ff95fdd643ec1a50465a0e16ae9
parentd5ffe3e5645efa1737d659a564c54b45fff829d5 (diff)
downloadpasslib-713c0bb81f3cec4ee15715657c627a9757a2edf2.tar.gz
PasswordHash.hash() api shift: deprecating passing settings kwds into hash() --
callers should use handler.replace(**settings).hash() instead. this is being done because it greatly streamlines the internals of the .hash() implementation, and allows some redundant configuration parsing to be extracted from the .hash() methods and merged in with existing code in .replace(). this also opens things up for alternate code architectures for implementing new hashers, making it easier to wrap existing libraries (e.g. argon2). internals --------- * replaced a bunch of internal .hash(**settings) calls * GenericHandler - stripped out 'relaxed' keyword from constructor, since it's no longer passed by hash() etc. - _norm_checksum() now only invoked if checksum is specified (simplifies logic). keeping support for 'relaxed' mode, but only as explicit keyword. - removed some unused comments about .from_string() & .to_string() * HasSalt mixin: - .replace() now supports 'salt' keyword, creates variant which has a fixed salt string. - 'salt size' keyword removed from ctor, now handled by .replace() call - _norm_salt() converted to class method so it can be used by .replace() 'salt' keyword code. - per-instance bits of _norm_salt() relocated to HasSalt.__init__ proper - _generate_salt() converted to class method, since no longer depends on instance config. * HasRounds mixin: - similar to HasSalt, relocates per-instance bits of _norm_rounds() into HasRounds.__init__() proper. - remainder of _norm_rounds() turned into class method, merged with ._clip_to_valid_rounds() helper to reduce duplication. - _generate_rounds() converted to class method, since no longer depends on instance config. hashers ------- * fshp: added support for 'variant' keyword to replace() * unix_disabled: added support for 'marker' keyword to replace(), added UTs. * cisco_type7: to match HasSalt, added support for 'salt' keyword to replace(), added UTs. * sha256/512_crypt: now uses custom salt & rounds parsing, rather than relaxed kwd, to handle correctable-but-invalid config strings. unittests --------- * removed checks for PasslibConfigWarning when setting hash(rounds=) out of policy bounds, since that now *is* setting the policy. * adapted some handler ctor to deal w/ lack of 'relaxed' kwd docs ---- * updated docstrings listing hash() keywords for each scheme to list them as .replace() keywords. * updated example code to use .replace() * fleshed out api docs about the change
-rw-r--r--CHANGES12
-rw-r--r--admin/benchmarks.py33
-rw-r--r--choose_rounds.py2
-rw-r--r--docs/lib/passlib.context-tutorial.rst12
-rw-r--r--docs/lib/passlib.hash.bcrypt.rst2
-rw-r--r--docs/lib/passlib.hash.bcrypt_sha256.rst2
-rw-r--r--docs/lib/passlib.hash.bsdi_crypt.rst2
-rw-r--r--docs/lib/passlib.hash.fshp.rst2
-rw-r--r--docs/lib/passlib.hash.md5_crypt.rst2
-rw-r--r--docs/lib/passlib.hash.pbkdf2_digest.rst2
-rw-r--r--docs/lib/passlib.hash.scram.rst4
-rw-r--r--docs/lib/passlib.hash.scrypt.rst2
-rw-r--r--docs/lib/passlib.hash.sha256_crypt.rst2
-rw-r--r--docs/password_hash_api.rst43
-rw-r--r--passlib/handlers/bcrypt.py18
-rw-r--r--passlib/handlers/cisco.py59
-rw-r--r--passlib/handlers/des_crypt.py27
-rw-r--r--passlib/handlers/django.py4
-rw-r--r--passlib/handlers/fshp.py12
-rw-r--r--passlib/handlers/ldap_digests.py4
-rw-r--r--passlib/handlers/md5_crypt.py4
-rw-r--r--passlib/handlers/misc.py30
-rw-r--r--passlib/handlers/mssql.py4
-rw-r--r--passlib/handlers/oracle.py2
-rw-r--r--passlib/handlers/pbkdf2.py10
-rw-r--r--passlib/handlers/phpass.py2
-rw-r--r--passlib/handlers/scram.py8
-rw-r--r--passlib/handlers/scrypt.py8
-rw-r--r--passlib/handlers/sha1_crypt.py2
-rw-r--r--passlib/handlers/sha2_crypt.py15
-rw-r--r--passlib/handlers/sun_md5_crypt.py2
-rw-r--r--passlib/ifc.py22
-rw-r--r--passlib/tests/test_context.py8
-rw-r--r--passlib/tests/test_handlers.py12
-rw-r--r--passlib/tests/test_handlers_bcrypt.py2
-rw-r--r--passlib/tests/test_utils_handlers.py31
-rw-r--r--passlib/tests/utils.py9
-rw-r--r--passlib/utils/handlers.py315
38 files changed, 407 insertions, 325 deletions
diff --git a/CHANGES b/CHANGES
index 5ab73ef..ac33eed 100644
--- a/CHANGES
+++ b/CHANGES
@@ -91,6 +91,10 @@ Minor Internal Changes
Deprecations
------------
+ Passlib 1.7 has undergone a large number of deprecations, as part of a long range plan
+ to restructure and simplify both the API and the internals of Passlib 2.0. There will be
+ at least one more major release (1.8 or 1.9) before Passlib 2.0 is released.
+
* The :class:`~passlib.ifc.PasswordHash` API (used by all hashes in passlib),
has had a number of cleanups made:
@@ -103,9 +107,11 @@ Deprecations
:meth:`~passlib.ifc.PasswordHash.genconfig` have been deprecated.
Compatibility aliases will remain in place until Passlib 2.0.
- * In order for :meth:`~passlib.ifc.PasswordHash.hash` to take over the job
- previously performed by :meth:`~passlib.ifc.PasswordHash.genhash`, it now accepts
- a keyword ``"config"``, which specifies an existing hash to extract configuration from.
+ * Settings options like ``rounds`` and ``salt_size`` should no longer be passed to
+ :meth:`!hash`. Instead, callers should use the new :meth:`~passlib.ifc.PasswordHash.replace`
+ method: for example, ``sha256_crypt.hash("secret", rounds=12345)`` should now be
+ ``sha256_crypt.replace(rounds=12345).hash("secret")``. Support for the old method
+ will be removed in Passlib 2.0.
* The :class:`~passlib.context.CryptContext` object has a number of cleanups made:
diff --git a/admin/benchmarks.py b/admin/benchmarks.py
index c6519e7..c7ce7b1 100644
--- a/admin/benchmarks.py
+++ b/admin/benchmarks.py
@@ -180,11 +180,12 @@ def test_context_calls():
schemes=[BlankHandler, AnotherHandler],
default="another",
blank__min_rounds=1500,
+ blank__default_rounds=2001,
blank__max_rounds=2500,
another__vary_rounds=100,
)
def helper():
- hash = ctx.hash(SECRET, rounds=2001)
+ hash = ctx.hash(SECRET)
ctx.verify(SECRET, hash)
ctx.verify_and_update(SECRET, hash)
ctx.verify_and_update(OTHER, hash)
@@ -200,11 +201,11 @@ def test_bcrypt_builtin():
import os
os.environ['PASSLIB_BUILTIN_BCRYPT'] = 'enabled'
bcrypt.set_backend("builtin")
- bcrypt.default_rounds = 10
+ handler = bcrypt.replace(rounds = 10)
def helper():
- hash = bcrypt.hash(SECRET)
- bcrypt.verify(SECRET, hash)
- bcrypt.verify(OTHER, hash)
+ hash = handler.hash(SECRET)
+ handler.verify(SECRET, hash)
+ handler.verify(OTHER, hash)
return helper
@benchmark.constructor()
@@ -212,11 +213,11 @@ def test_bcrypt_ffi():
"test bcrypt 'bcrypt' backend"
from passlib.hash import bcrypt
bcrypt.set_backend("bcrypt")
- bcrypt.default_rounds = 8
+ handler = bcrypt.replace(rounds=8)
def helper():
- hash = bcrypt.hash(SECRET)
- bcrypt.verify(SECRET, hash)
- bcrypt.verify(OTHER, hash)
+ hash = handler.hash(SECRET)
+ handler.verify(SECRET, hash)
+ handler.verify(OTHER, hash)
return helper
@benchmark.constructor()
@@ -235,7 +236,7 @@ def test_ldap_salted_md5():
"""test ldap_salted_md5"""
from passlib.hash import ldap_salted_md5 as handler
def helper():
- hash = handler.hash(SECRET, salt='....')
+ hash = handler.hash(SECRET)
handler.verify(SECRET, hash)
handler.verify(OTHER, hash)
return helper
@@ -243,20 +244,20 @@ def test_ldap_salted_md5():
@benchmark.constructor()
def test_phpass():
"""test phpass"""
- from passlib.hash import phpass as handler
- kwds = dict(salt='.'*8, rounds=16)
+ from passlib.hash import phpass
+ handler = phpass.replace(salt='.'*8, rounds=16)
def helper():
- hash = handler.hash(SECRET, **kwds)
+ hash = handler.hash(SECRET)
handler.verify(SECRET, hash)
handler.verify(OTHER, hash)
return helper
@benchmark.constructor()
def test_sha1_crypt():
- from passlib.hash import sha1_crypt as handler
- kwds = dict(salt='.'*8, rounds=10000)
+ from passlib.hash import sha1_crypt
+ handler = sha1_crypt.replace(salt='.'*8, rounds=10000)
def helper():
- hash = handler.hash(SECRET, **kwds)
+ hash = handler.hash(SECRET)
handler.verify(SECRET, hash)
handler.verify(OTHER, hash)
return helper
diff --git a/choose_rounds.py b/choose_rounds.py
index e8607e9..21cb3ce 100644
--- a/choose_rounds.py
+++ b/choose_rounds.py
@@ -103,7 +103,7 @@ def main(*args):
"""estimate speed using specified # of rounds"""
# time a single verify() call
secret = "S0m3-S3Kr1T"
- hash = hasher.hash(secret, rounds=rounds)
+ hash = hasher.replace(rounds=rounds).hash(secret)
def helper():
start = tick()
hasher.verify(secret, hash)
diff --git a/docs/lib/passlib.context-tutorial.rst b/docs/lib/passlib.context-tutorial.rst
index 2e7cc02..f9a5fce 100644
--- a/docs/lib/passlib.context-tutorial.rst
+++ b/docs/lib/passlib.context-tutorial.rst
@@ -346,17 +346,7 @@ New hashes generated by this context will always honor the minimum
>>> myctx.needs_update(hash1)
False
-Explicitly setting the rounds too low will cause a warning,
-and the minimum will be used anyways::
-
- >>> # explicit rounds passed to encrypt...
- >>> myctx.hash("password", rounds=1000)
- __main__:1: PasslibConfigWarning: sha256_crypt config requires rounds >= 131072,
- increasing value from 80000
- '$5$rounds=131072$86YrzUF3fGwY99oy$03e/pyh4l3N/G0509er9JiQmIxc0y9lrAJaLswX/iv8'
- ^^^^^^
-
-But if an existing hash below the minimum is tested, it will show up as needing rehashing::
+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'
diff --git a/docs/lib/passlib.hash.bcrypt.rst b/docs/lib/passlib.hash.bcrypt.rst
index 039b178..68f6aea 100644
--- a/docs/lib/passlib.hash.bcrypt.rst
+++ b/docs/lib/passlib.hash.bcrypt.rst
@@ -19,7 +19,7 @@ for new applications. This class can be used directly as follows::
'$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy'
>>> # the same, but with an explicit number of rounds
- >>> bcrypt.hash("password", rounds=8)
+ >>> bcrypt.replace(rounds=8).hash("password")
'$2a$08$8wmNsdCH.M21f.LSBSnYjQrZ9l1EmtBc9uNPGL.9l75YE8D8FlnZC'
>>> # verify password
diff --git a/docs/lib/passlib.hash.bcrypt_sha256.rst b/docs/lib/passlib.hash.bcrypt_sha256.rst
index 8b8face..6d9fd7c 100644
--- a/docs/lib/passlib.hash.bcrypt_sha256.rst
+++ b/docs/lib/passlib.hash.bcrypt_sha256.rst
@@ -21,7 +21,7 @@ This class can be used directly as follows::
'$bcrypt-sha256$2a,12$LrmaIX5x4TRtAwEfwJZa1.$2ehnw6LvuIUTM0iz4iz9hTxv21B6KFO'
>>> # the same, but with an explicit number of rounds
- >>> bcrypt.hash("password", rounds=8)
+ >>> bcrypt.replace(rounds=8).hash("password")
'$bcrypt-sha256$2a,8$UE3dIZ.0I6XZtA/LdMrrle$Ag04/5zYu./12.OSqInXZnJ.WZoh1ua'
>>> # verify password
diff --git a/docs/lib/passlib.hash.bsdi_crypt.rst b/docs/lib/passlib.hash.bsdi_crypt.rst
index 082fd53..88bb49f 100644
--- a/docs/lib/passlib.hash.bsdi_crypt.rst
+++ b/docs/lib/passlib.hash.bsdi_crypt.rst
@@ -22,7 +22,7 @@ It class can be used directly as follows::
'_7C/.Bf/4gZk10RYRs4Y'
>>> # same, but with explict number of rounds
- >>> bsdi_crypt.hash("password", rounds=10001)
+ >>> bsdi_crypt.replace(rounds=10001).hash("password")
'_FQ0.amG/zwCMip7DnBk'
>>> # verify password
diff --git a/docs/lib/passlib.hash.fshp.rst b/docs/lib/passlib.hash.fshp.rst
index 5fd88db..640bb35 100644
--- a/docs/lib/passlib.hash.fshp.rst
+++ b/docs/lib/passlib.hash.fshp.rst
@@ -29,7 +29,7 @@ It can be used directly as follows::
'{FSHP1|16|16384}PtoqcGUetmVEy/uR8715TNqKa8+teMF9qZO1lA9lJNUm1EQBLPZ+qPRLeEPHqy6C'
>>> # the same, but with an explicit number of rounds, larger salt, and specific variant
- >>> fshp.hash("password", rounds=40000, salt_size=32, variant="sha512")
+ >>> fshp.replace(rounds=40000, salt_size=32, variant="sha512").hash("password")
'{FSHP3|32|40000}cB8yE/CuADSgUTQZjWy+YTf/cvbU11D/rHNKiUiB6z4dIaO77U/rmNW
pgZcZllZbCra5GJ8ZfFRNwCHirPqvYTAnbaQQeFQbWym/frRrRev3buoygFQRYexl4091Pc5m'
diff --git a/docs/lib/passlib.hash.md5_crypt.rst b/docs/lib/passlib.hash.md5_crypt.rst
index 3105c26..79cdad6 100644
--- a/docs/lib/passlib.hash.md5_crypt.rst
+++ b/docs/lib/passlib.hash.md5_crypt.rst
@@ -43,7 +43,7 @@ The :class:`!md5_crypt` class can be can be used directly as follows::
False
>>> # encrypt password using cisco-compatible 4-char salt
- >>> md5_crypt.hash("password", salt_size=4)
+ >>> md5_crypt.replace(salt_size=4).hash("password")
'$1$wu98$9UuD3hvrwehnqyF1D548N0'
.. seealso::
diff --git a/docs/lib/passlib.hash.pbkdf2_digest.rst b/docs/lib/passlib.hash.pbkdf2_digest.rst
index 8c5bca6..44e5419 100644
--- a/docs/lib/passlib.hash.pbkdf2_digest.rst
+++ b/docs/lib/passlib.hash.pbkdf2_digest.rst
@@ -32,7 +32,7 @@ All of these classes can be used directly as follows::
'$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M'
>>> # same, but with an explicit number of rounds and salt length
- >>> pbkdf2_sha256.hash("password", rounds=8000, salt_size=10)
+ >>> pbkdf2_sha256.replace(rounds=8000, salt_size=10).hash("password")
'$pbkdf2-sha256$8000$XAuBMIYQQogxRg$tRRlz8hYn63B9LYiCd6PRo6FMiunY9ozmMMI3srxeRE'
>>> # verify the password
diff --git a/docs/lib/passlib.hash.scram.rst b/docs/lib/passlib.hash.scram.rst
index b1b6c3c..2836970 100644
--- a/docs/lib/passlib.hash.scram.rst
+++ b/docs/lib/passlib.hash.scram.rst
@@ -44,7 +44,7 @@ This class can be used like any other Passlib hash, as follows::
gzvGjbMeuWCtKve8TPjRMNoZK9EGyHQ6y0lW9OtWdHZrDZbBUhB9ou./VI2mlw'
>>> # same, but with an explicit number of rounds
- >>> scram.hash("password", rounds=8000)
+ >>> scram.replace(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'
@@ -64,7 +64,7 @@ Additionally, this class provides a number of useful methods for SCRAM-specific
* You can override the default list of digests, and/or the number of iterations::
- >>> hash = scram.hash("password", rounds=1000, algs="sha-1,sha-256,md5")
+ >>> hash = scram.replace(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'
diff --git a/docs/lib/passlib.hash.scrypt.rst b/docs/lib/passlib.hash.scrypt.rst
index 9629af7..b944360 100644
--- a/docs/lib/passlib.hash.scrypt.rst
+++ b/docs/lib/passlib.hash.scrypt.rst
@@ -27,7 +27,7 @@ This class can be used directly as follows::
'$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy'
>>> # the same, but with an explicit number of rounds
- >>> scrypt.hash("password", rounds=8)
+ >>> scrypt.replace(rounds=8).hash("password")
'$scrypt$16,8,1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD.iCs5E'
>>> # verify password
diff --git a/docs/lib/passlib.hash.sha256_crypt.rst b/docs/lib/passlib.hash.sha256_crypt.rst
index 069fc19..32c683c 100644
--- a/docs/lib/passlib.hash.sha256_crypt.rst
+++ b/docs/lib/passlib.hash.sha256_crypt.rst
@@ -23,7 +23,7 @@ This class can be used directly as follows::
'$5$rounds=80000$wnsT7Yr92oJoP28r$cKhJImk5mfuSKV9b3mumNzlbstFUplKtQXXMo4G6Ep5'
>>> # same, but with explict number of rounds
- >>> sha256_crypt.hash("password", rounds=12345)
+ >>> sha256_crypt.replace(rounds=12345).hash("password")
'$5$rounds=12345$q3hvJE5mn5jKRsW.$BbbYTFiaImz9rTy03GGi.Jf9YY5bmxN0LU3p3uI1iUB'
>>> # verify password
diff --git a/docs/password_hash_api.rst b/docs/password_hash_api.rst
index 98be79b..2140e5f 100644
--- a/docs/password_hash_api.rst
+++ b/docs/password_hash_api.rst
@@ -57,8 +57,8 @@ using the :class:`~passlib.hash.pbkdf2_sha256` hash as an example::
'$pbkdf2-sha256$29000$njNmDCGEUIoRwvi/1/ofQw$nYU.7v.fvG9UyT.7sTMbWSG98KSm/Tr4rS9Ob5UkYPw
>>> # if the hash supports a variable number of iterations (which pbkdf2_sha256 does),
- >>> # you can override the default value via the 'rounds' keyword:
- >>> pbkdf2_sha256.hash("password", rounds=12345)
+ >>> # you can override the default using the replace() method and the 'rounds' keyword:
+ >>> pbkdf2_sha256.replace(rounds=12345).hash("password")
'$pbkdf2-sha256$12345$QwjBmJPSOsf4HyNE6L239g$8m1pnP69EYeOiKKb5sNSiYw9M8pJMyeW.CSm0KKO.GI'
^^^^^
@@ -156,7 +156,15 @@ and hash comparison.
in that hash's documentation; though many of the more common keywords
are listed under :attr:`~PasswordHash.setting_kwds`
and :attr:`~PasswordHash.context_kwds`.
- Examples of common keywords include ``rounds`` and ``salt_size``.
+
+ .. 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.replace(**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.
@@ -191,6 +199,7 @@ and hash comparison.
.. versionchanged:: 1.7
This method was renamed from :meth:`encrypt`.
+ Deprecated support for passing settings directly into :meth:`!hash`.
.. classmethod:: PasswordHash.encrypt(secret, \*\*kwds)
@@ -257,12 +266,12 @@ and hash comparison.
instead of a properly-formed hash; previous releases were inconsistent
in their handling of these two border cases.
-.. classmethod:: PasswordHash.replace(\*\*settings)
+.. classmethod:: PasswordHash.replace(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 \*\*kwds:
+ :param \*\*settings:
All keywords are algorithm-specific, and will be listed
in that hash's documentation; though many of the more common keywords
@@ -274,14 +283,14 @@ and hash comparison.
:raises ValueError:
- * If a ``kwd``'s value is invalid (e.g. if a ``salt`` string
+ * 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.
- .. versionadd:: 1.7
+ .. versionadded:: 1.7
.. _hash-unicode-behavior:
@@ -484,9 +493,11 @@ the hashes in passlib:
.. attribute:: PasswordHash.setting_kwds
- Tuple listing the keywords supported by :meth:`~PasswordHash.hash`
- and :meth:`~PasswordHash.genconfig` that control hash generation, and which will
- be encoded into the resulting hash.
+ Tuple listing the keywords supported by :meth:`~PasswordHash.replace` 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,
@@ -563,7 +574,7 @@ the hashes in passlib:
.. _relaxed-keyword:
``relaxed``
- By default, passing an invalid value to :meth:`~PasswordHash.hash`
+ By default, passing an invalid value to :meth:`~PasswordHash.replace`
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.
@@ -572,8 +583,6 @@ the hashes in passlib:
and ``salt_size`` values that are too low or too high, ``salt``
strings that are too large.
- This option is supported by most of the hashes in Passlib.
-
.. versionadded:: 1.6
.. _context-keywords:
@@ -581,14 +590,16 @@ the hashes in passlib:
.. attribute:: PasswordHash.context_kwds
Tuple listing the keywords supported by :meth:`~PasswordHash.hash`,
- :meth:`~PasswordHash.verify`, and :meth:`~PasswordHash.genhash` affect the hash, but are
- not encoded within it, and thus must be provided each time
+ :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.
- This is typically an empty tuple for most of the hashes in passlib.
+
+ *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,
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index 00057aa..fe34298 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -87,7 +87,7 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
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 the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -227,14 +227,16 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
else:
return hash
- def _generate_salt(self, salt_size):
+ @classmethod
+ def _generate_salt(cls):
# 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)
+ salt = super(bcrypt, cls)._generate_salt()
return bcrypt64.repair_unused(salt)
- def _norm_salt(self, salt, **kwds):
- salt = super(bcrypt, self)._norm_salt(salt, **kwds)
+ @classmethod
+ def _norm_salt(cls, salt, **kwds):
+ salt = super(bcrypt, cls)._norm_salt(salt, **kwds)
assert salt is not None, "HasSalt didn't generate new salt!"
changed, salt = bcrypt64.check_repair_unused(salt)
if changed:
@@ -247,10 +249,8 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
PasslibHashWarning)
return salt
- def _norm_checksum(self, checksum):
- checksum = super(bcrypt, self)._norm_checksum(checksum)
- if not checksum:
- return None
+ def _norm_checksum(self, checksum, relaxed=False):
+ checksum = super(bcrypt, self)._norm_checksum(checksum, relaxed=relaxed)
changed, checksum = bcrypt64.check_repair_unused(checksum)
if changed:
warn(
diff --git a/passlib/handlers/cisco.py b/passlib/handlers/cisco.py
index b382158..a12da85 100644
--- a/passlib/handlers/cisco.py
+++ b/passlib/handlers/cisco.py
@@ -128,8 +128,7 @@ class cisco_type7(uh.GenericHandler):
It has a simple 4-5 bit salt, but is nonetheless a reversible encoding
instead of a real hash.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genhash` methods
- have the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: int
:param salt:
@@ -167,6 +166,14 @@ class cisco_type7(uh.GenericHandler):
# methods
#===================================================================
@classmethod
+ def replace(cls, salt=None, **kwds):
+ subcls = super(cisco_type7, cls).replace(**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:
@@ -176,29 +183,35 @@ class cisco_type7(uh.GenericHandler):
def __init__(self, salt=None, **kwds):
super(cisco_type7, self).__init__(**kwds)
- self.salt = self._norm_salt(salt)
-
- def _norm_salt(self, salt):
- """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.
- if salt is None:
- if self.use_defaults:
- salt = self._generate_salt()
- else:
- raise TypeError("no salt specified")
+ 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 salt < 0 or salt > self.max_salt_value:
- msg = "salt/offset must be in 0..52 range"
- if self.relaxed:
- warn(msg, uh.PasslibHashWarning)
- salt = 0 if salt < 0 else self.max_salt_value
- else:
- raise ValueError(msg)
- return salt
-
- def _generate_salt(self):
+ 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):
diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py
index 1de21e1..a6aae6b 100644
--- a/passlib/handlers/des_crypt.py
+++ b/passlib/handlers/des_crypt.py
@@ -113,7 +113,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -210,7 +210,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
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 the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -296,17 +296,18 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
# want to eventually expose rounds logic to that script in better way.
_avoid_even_rounds = True
- def _norm_rounds(self, rounds):
- rounds = super(bsdi_crypt, self)._norm_rounds(rounds)
+ def _parse_rounds(self, rounds):
+ rounds = super(bsdi_crypt, self)._parse_rounds(rounds)
# issue warning if app provided an even rounds value
- if self.use_defaults and not rounds & 1:
+ if not rounds & 1:
warn("bsdi_crypt rounds should be odd, "
"as even rounds may reveal weak DES keys",
uh.exc.PasslibSecurityWarning)
return rounds
- def _generate_rounds(self):
- rounds = super(bsdi_crypt, self)._generate_rounds()
+ @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.
@@ -369,7 +370,7 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -422,11 +423,11 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
hash = u("%s%s") % (self.salt, self.checksum)
return uascii_to_str(hash)
- def _norm_checksum(self, value):
- value = super(bigcrypt, self)._norm_checksum(value)
- if value and len(value) % 11:
+ 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 value
+ return checksum
#===================================================================
# backend
@@ -452,7 +453,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py
index 4f2dcfc..e86ddc9 100644
--- a/passlib/handlers/django.py
+++ b/passlib/handlers/django.py
@@ -237,7 +237,7 @@ class django_pbkdf2_sha256(DjangoVariableHash):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -288,7 +288,7 @@ class django_pbkdf2_sha1(django_pbkdf2_sha256):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/fshp.py b/passlib/handlers/fshp.py
index 553ec57..25cd247 100644
--- a/passlib/handlers/fshp.py
+++ b/passlib/handlers/fshp.py
@@ -27,7 +27,7 @@ class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:param salt:
Optional raw salt string.
@@ -98,6 +98,16 @@ class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
)
#===================================================================
+ # configuration
+ #===================================================================
+ @classmethod
+ def replace(cls, variant=None, **kwds):
+ subcls = super(fshp, cls).replace(**kwds)
+ if variant is not None:
+ subcls.default_variant = cls(use_defaults=True)._norm_variant(variant)
+ return subcls
+
+ #===================================================================
# instance attrs
#===================================================================
variant = None
diff --git a/passlib/handlers/ldap_digests.py b/passlib/handlers/ldap_digests.py
index 6e5c54b..ec39fb2 100644
--- a/passlib/handlers/ldap_digests.py
+++ b/passlib/handlers/ldap_digests.py
@@ -124,7 +124,7 @@ class ldap_salted_md5(_SaltedBase64DigestHelper):
It supports a 4-16 byte salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
@@ -163,7 +163,7 @@ class ldap_salted_sha1(_SaltedBase64DigestHelper):
It supports a 4-16 byte salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
diff --git a/passlib/handlers/md5_crypt.py b/passlib/handlers/md5_crypt.py
index fd7d9fa..d5cb9cc 100644
--- a/passlib/handlers/md5_crypt.py
+++ b/passlib/handlers/md5_crypt.py
@@ -226,7 +226,7 @@ class md5_crypt(uh.HasManyBackends, _MD5_Common):
It supports a variable-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -304,7 +304,7 @@ class apr_md5_crypt(_MD5_Common):
It supports a variable-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/misc.py b/passlib/handlers/misc.py
index f494ddd..a9057a9 100644
--- a/passlib/handlers/misc.py
+++ b/passlib/handlers/misc.py
@@ -106,8 +106,7 @@ class unix_disabled(uh.MinimalHandler):
setting_kwds = ("marker",)
context_kwds = ()
- # XXX: could subclass replace() to allow setting custom default_marker
-
+ # TODO: rename attr to 'marker'...
if 'bsd' in sys.platform: # pragma: no cover -- runtime detection
default_marker = u("*")
else:
@@ -117,6 +116,15 @@ class unix_disabled(uh.MinimalHandler):
default_marker = u("!")
@classmethod
+ def replace(cls, marker=None, **kwds):
+ subcls = super(unix_disabled, cls).replace(**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".
@@ -147,15 +155,13 @@ class unix_disabled(uh.MinimalHandler):
return False
@classmethod
- def hash(cls, secret, marker=None):
+ def hash(cls, secret, **kwds):
+ if kwds:
+ uh.warn_hash_settings_deprecation(cls, kwds)
+ return cls.replace(**kwds).hash(secret)
uh.validate_secret(secret)
- # if None or empty string, replace with marker
- if marker:
- if not cls.identify(marker):
- raise ValueError("invalid marker: %r" % marker)
- else:
- marker = cls.default_marker
- assert marker and cls.identify(marker)
+ 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")
@@ -168,7 +174,9 @@ class unix_disabled(uh.MinimalHandler):
uh.validate_secret(secret)
return to_native_str(config, param="config")
else:
- return cls.hash(secret, marker=marker)
+ if marker is not None:
+ cls = cls.replace(marker=marker)
+ return cls.hash(secret)
class plaintext(uh.MinimalHandler):
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
diff --git a/passlib/handlers/mssql.py b/passlib/handlers/mssql.py
index 5366618..0c61333 100644
--- a/passlib/handlers/mssql.py
+++ b/passlib/handlers/mssql.py
@@ -104,7 +104,7 @@ class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
@@ -181,7 +181,7 @@ class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
diff --git a/passlib/handlers/oracle.py b/passlib/handlers/oracle.py
index 3e7f4f9..0e567da 100644
--- a/passlib/handlers/oracle.py
+++ b/passlib/handlers/oracle.py
@@ -106,7 +106,7 @@ class oracle11(uh.HasSalt, uh.GenericHandler):
It supports a fixed-length salt.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/pbkdf2.py b/passlib/handlers/pbkdf2.py
index 4a92767..1bd9e86 100644
--- a/passlib/handlers/pbkdf2.py
+++ b/passlib/handlers/pbkdf2.py
@@ -95,7 +95,7 @@ def create_pbkdf2_hash(hash_name, digest_size, rounds=12000, ident=None, module=
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
@@ -148,7 +148,7 @@ class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.Generic
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
@@ -245,7 +245,7 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -351,7 +351,7 @@ class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler)
It supports a fixed-length salt, and a fixed number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keyword:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
@@ -407,7 +407,7 @@ class grub_pbkdf2_sha512(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.Gene
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
diff --git a/passlib/handlers/phpass.py b/passlib/handlers/phpass.py
index eb3e46e..de32e99 100644
--- a/passlib/handlers/phpass.py
+++ b/passlib/handlers/phpass.py
@@ -29,7 +29,7 @@ class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
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 the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py
index 1b9a25d..b2768df 100644
--- a/passlib/handlers/scram.py
+++ b/passlib/handlers/scram.py
@@ -25,7 +25,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: bytes
:param salt:
@@ -304,9 +304,9 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
super(scram, self).__init__(**kwds)
self.algs = self._norm_algs(algs)
- def _norm_checksum(self, checksum):
- if checksum is None:
- return None
+ 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" %
diff --git a/passlib/handlers/scrypt.py b/passlib/handlers/scrypt.py
index 1ea0c94..2325600 100644
--- a/passlib/handlers/scrypt.py
+++ b/passlib/handlers/scrypt.py
@@ -26,7 +26,7 @@ class scrypt(uh.HasRounds, uh.HasRawChecksum, uh.HasRawSalt, uh.GenericHandler):
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.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -177,7 +177,7 @@ class scrypt(uh.HasRounds, uh.HasRawChecksum, uh.HasRawSalt, uh.GenericHandler):
return self._norm_integer(parallel_count, self.parallel_count, "parallel_count")
# XXX: this might be generally useful, could move to utils.handlers...
- def _norm_integer(self, value, default, param, min=1, max=None):
+ def _norm_integer(self, value, default, param, min=1, max=None, relaxed=False):
"""
helper to normalize and validate an integer value
@@ -203,7 +203,7 @@ class scrypt(uh.HasRounds, uh.HasRawChecksum, uh.HasRawSalt, uh.GenericHandler):
# check min bound
if value < min:
msg = "%s too low (%s requires %s >= %d)" % (param, self.name, param, min)
- if self.relaxed:
+ if relaxed:
warn(msg, uh.exc.PasslibHashWarning)
value = min
else:
@@ -212,7 +212,7 @@ class scrypt(uh.HasRounds, uh.HasRawChecksum, uh.HasRawSalt, uh.GenericHandler):
# check max bound
if max and value > max:
msg = "%s too high (%s requires %s <= %d)" % (param, self.name, param, max)
- if self.relaxed:
+ if relaxed:
warn(msg, uh.exc.PasslibHashWarning)
value = max
else:
diff --git a/passlib/handlers/sha1_crypt.py b/passlib/handlers/sha1_crypt.py
index 3db70ad..0626441 100644
--- a/passlib/handlers/sha1_crypt.py
+++ b/passlib/handlers/sha1_crypt.py
@@ -26,7 +26,7 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py
index db321b6..5ec4703 100644
--- a/passlib/handlers/sha2_crypt.py
+++ b/passlib/handlers/sha2_crypt.py
@@ -286,6 +286,14 @@ class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt,
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 -
@@ -329,9 +337,6 @@ class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt,
salt=salt,
checksum=chk or None,
implicit_rounds=implicit_rounds,
- relaxed=not chk, # NOTE: relaxing parsing for config strings
- # so that out-of-range rounds are clipped,
- # since SHA2-Crypt spec treats them this way.
)
def to_string(self):
@@ -394,7 +399,7 @@ class sha256_crypt(_SHA2_Common):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
@@ -453,7 +458,7 @@ class sha512_crypt(_SHA2_Common):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py
index e5e6ac9..afe237c 100644
--- a/passlib/handlers/sun_md5_crypt.py
+++ b/passlib/handlers/sun_md5_crypt.py
@@ -176,7 +176,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
It supports a variable-length salt, and a variable number of rounds.
- The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept the following optional keywords:
+ The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
:type salt: str
:param salt:
diff --git a/passlib/ifc.py b/passlib/ifc.py
index 63dd58e..becf7e6 100644
--- a/passlib/ifc.py
+++ b/passlib/ifc.py
@@ -81,7 +81,27 @@ class PasswordHash(object):
@abstractmethod
def hash(cls, secret, # *
**setting_and_context_kwds): # pragma: no cover -- abstract method
- """encrypt secret, returning resulting hash"""
+ """
+ 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
+ ``.replace(**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")
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py
index 4c4677c..1c0b03b 100644
--- a/passlib/tests/test_context.py
+++ b/passlib/tests/test_context.py
@@ -985,7 +985,7 @@ sha512_crypt__min_rounds = 45000
# NOTE: more thorough job of rounds limits done below.
# min rounds
- with self.assertWarningList(PasslibConfigWarning):
+ with self.assertWarningList([]):
self.assertEqual(
cc.hash("password", rounds=1999, salt="nacl"),
'$5$rounds=1999$nacl$nmfwJIxqj0csloAAvSER0B8LU0ERCAbhmMug4Twl609',
@@ -1333,7 +1333,8 @@ sha512_crypt__min_rounds = 45000
self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$" + STUB)
# below policy minimum
- with self.assertWarningList(PasslibConfigWarning):
+ # 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)
@@ -1357,7 +1358,8 @@ sha512_crypt__min_rounds = 45000
self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=999999999$nacl$" + STUB)
# above policy max
- with self.assertWarningList(PasslibConfigWarning):
+ # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .replace()
+ with self.assertWarningList([]):
self.assertEqual(
cc.genconfig(rounds=3001, salt="nacl"), '$5$rounds=3001$nacl$' + STUB)
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index cd0f7b7..129a487 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -393,9 +393,14 @@ class cisco_type7_test(HandlerCase):
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.replace, salt='abc')
+ self.assertRaises(ValueError, handler.replace, salt=-10)
+ self.assertRaises(ValueError, handler.replace, salt=100)
with self.assertWarningList("salt/offset must be.*"):
- h = handler(salt=100, relaxed=True)
- self.assertEqual(h.salt, 52)
+ subcls = handler.replace(salt=100, relaxed=True)
+ self.assertEqual(subcls(use_defaults=True).salt, 52)
#=============================================================================
# crypt16
@@ -2368,14 +2373,17 @@ class unix_disabled_test(HandlerCase):
# use marker if no hash
self.assertEqual(handler.genhash("stub", ""), handler.default_marker)
self.assertEqual(handler.hash("stub"), handler.default_marker)
+ self.assertEqual(handler.replace().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.replace(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.replace, marker='abc')
class unix_fallback_test(HandlerCase):
handler = hash.unix_fallback
diff --git a/passlib/tests/test_handlers_bcrypt.py b/passlib/tests/test_handlers_bcrypt.py
index 9ed979e..fe3336c 100644
--- a/passlib/tests/test_handlers_bcrypt.py
+++ b/passlib/tests/test_handlers_bcrypt.py
@@ -342,7 +342,7 @@ class _bcrypt_test(HandlerCase):
for i in irange(6):
check_padding(bcrypt.genconfig())
for i in irange(3):
- check_padding(bcrypt.hash("bob", rounds=bcrypt.min_rounds))
+ check_padding(bcrypt.replace(rounds=bcrypt.min_rounds).hash("bob"))
#
# test genconfig() corrects invalid salts & issues warning.
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index 2812bae..1fa45cf 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -184,8 +184,8 @@ class SkeletonTest(TestCase):
checksum_size = 4
checksum_chars = u('xz')
- def norm_checksum(*a, **k):
- return d1(*a, **k).checksum
+ def norm_checksum(checksum=None, **k):
+ return d1(checksum=checksum, **k).checksum
# too small
self.assertRaises(ValueError, norm_checksum, u('xxx'))
@@ -204,9 +204,10 @@ class SkeletonTest(TestCase):
self.assertRaises(TypeError, norm_checksum, b'xxyx')
# relaxed
- with self.assertWarningList("checksum should be unicode"):
- self.assertEqual(norm_checksum(b'xxzx', relaxed=True), u('xxzx'))
- self.assertRaises(TypeError, norm_checksum, 1, relaxed=True)
+ # 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'))
@@ -225,7 +226,9 @@ class SkeletonTest(TestCase):
# test unicode
self.assertRaises(TypeError, norm_checksum, u('xxyx'))
- self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True)
+
+ # 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)
@@ -245,7 +248,7 @@ class SkeletonTest(TestCase):
return d1(**k).salt
def gen_salt(sz, **k):
- return d1(use_defaults=True, salt_size=sz, **k).salt
+ return d1.replace(salt_size=sz, **k)(use_defaults=True).salt
salts2 = _makelang('ab', 2)
salts3 = _makelang('ab', 3)
@@ -274,9 +277,6 @@ class SkeletonTest(TestCase):
self.assertRaises(ValueError, norm_salt, salt='aaaabb')
self.consumeWarningList(wlog)
- self.assertEqual(norm_salt(salt='aaaabb', relaxed=True), 'aaaa')
- self.consumeWarningList(wlog, PasslibHashWarning)
-
# check generated salts
with warnings.catch_warnings(record=True) as wlog:
@@ -296,7 +296,7 @@ class SkeletonTest(TestCase):
self.consumeWarningList(wlog)
self.assertIn(gen_salt(5, relaxed=True), salts4)
- self.consumeWarningList(wlog, ["salt too large"])
+ self.consumeWarningList(wlog, ["salt_size.*above max_salt_size"])
# test with max_salt_size=None
del d1.max_salt_size
@@ -306,7 +306,7 @@ class SkeletonTest(TestCase):
# TODO: test HasRawSalt mixin
- def test_30_norm_rounds(self):
+ def test_30_init_rounds(self):
"""test GenericHandler + HasRounds mixin"""
# setup helpers
class d1(uh.HasRounds, uh.GenericHandler):
@@ -316,6 +316,7 @@ class SkeletonTest(TestCase):
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
@@ -333,9 +334,6 @@ class SkeletonTest(TestCase):
self.assertRaises(ValueError, norm_rounds, rounds=0)
self.consumeWarningList(wlog)
- self.assertEqual(norm_rounds(rounds=0, relaxed=True), 1)
- self.consumeWarningList(wlog, PasslibHashWarning)
-
# just right
self.assertEqual(norm_rounds(rounds=1), 1)
self.assertEqual(norm_rounds(rounds=2), 2)
@@ -346,9 +344,6 @@ class SkeletonTest(TestCase):
self.assertRaises(ValueError, norm_rounds, rounds=4)
self.consumeWarningList(wlog)
- self.assertEqual(norm_rounds(rounds=4, relaxed=True), 3)
- self.consumeWarningList(wlog, PasslibHashWarning)
-
# check no default rounds
d1.default_rounds = None
self.assertRaises(TypeError, norm_rounds, use_defaults=True)
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 0d1a360..2f22bd7 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -1453,7 +1453,7 @@ class HandlerCase(TestCase):
# hack to bypass bsdi-crypt's "odd rounds only" behavior, messes up this test
orig_handler = handler
handler = handler.replace()
- handler._generate_rounds = lambda self: super(orig_handler, self)._generate_rounds()
+ 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
@@ -1550,7 +1550,7 @@ class HandlerCase(TestCase):
# NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .replace()
self.assertEqual(get_effective_rounds(subcls, small + 1), small + 1)
self.assertEqual(get_effective_rounds(subcls, small), small)
- with self.assertWarningList([PasslibConfigWarning]):
+ with self.assertWarningList([]):
self.assertEqual(get_effective_rounds(subcls, small - 1), small - 1)
# 'min_rounds' should be treated as alias for 'min_desired_rounds'
@@ -1606,10 +1606,11 @@ class HandlerCase(TestCase):
temp = subcls.replace(max_desired_rounds=large + 1)
self.assertEqual(temp.max_desired_rounds, large + 1)
- # hash() etc should allow explicit values above desired minimum, w/ warning
+ # 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 .replace()
self.assertEqual(get_effective_rounds(subcls, large - 1), large - 1)
self.assertEqual(get_effective_rounds(subcls, large), large)
- with self.assertWarningList([PasslibConfigWarning]):
+ with self.assertWarningList([]):
self.assertEqual(get_effective_rounds(subcls, large + 1), large + 1)
# 'max_rounds' should be treated as alias for 'max_desired_rounds'
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index c53c244..6eee98a 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -4,6 +4,7 @@
#=============================================================================
from __future__ import with_statement
# core
+import inspect
import logging; log = logging.getLogger(__name__)
import math
from warnings import warn
@@ -77,6 +78,37 @@ def _bitsize(count, chars):
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 not name.startswith("passlib.") or name.startswith("passlib.tests."):
+ 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.replace(**settings).hash(secret)' instead" % dict(handler=handler.name),
+ 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
#=============================================================================
@@ -464,35 +496,34 @@ class GenericHandler(MinimalHandler):
#===================================================================
# init
#===================================================================
- def __init__(self, checksum=None, use_defaults=False, relaxed=False,
- **kwds):
+ def __init__(self, checksum=None, use_defaults=False, **kwds):
self.use_defaults = use_defaults
- self.relaxed = relaxed
super(GenericHandler, self).__init__(**kwds)
- self.checksum = self._norm_checksum(checksum)
+ if checksum is not None:
+ # XXX: do we need to set .relaxed for checksum coercion?
+ self.checksum = self._norm_checksum(checksum)
- def _norm_checksum(self, 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.
- if checksum is None:
- return None
# normalize to bytes / unicode
raw = self._checksum_is_bytes
if raw:
- # NOTE: no clear route to reasonbly convert unicode -> raw bytes,
- # so relaxed does nothing here
+ # 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 self.relaxed:
- warn("checksum should be unicode, not bytes",
- PasslibHashWarning)
+ 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")
@@ -506,8 +537,7 @@ class GenericHandler(MinimalHandler):
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,))
+ raise ValueError("invalid characters in %s checksum" % (self.name,))
return checksum
@@ -560,29 +590,12 @@ class GenericHandler(MinimalHandler):
"""render instance to hash or configuration string
:returns:
- if :attr:`checksum` is set, should return full hash string.
- if not, should either return abbreviated configuration string,
- or fill in a stub checksum.
+ hash string with salt & digest included.
should return native string type (ascii-bytes under python 2,
unicode under python 3)
"""
- # NOTE: documenting some non-standardized but common kwd flags
- # that passlib to_string() method may have:
- #
- # withchk=True -- if false, omit checksum portion of hash
- #
- raise NotImplementedError("%s must implement from_string()" %
- (self.__class__,))
-
- ##def to_config_string(self):
- ## "helper for generating configuration string (ignoring hash)"
- ## orig = self.checksum
- ## try:
- ## self.checksum = None
- ## return self.to_string()
- ## finally:
- ## self.checksum = orig
+ raise NotImplementedError("%s must implement from_string()" % (self.__class__,))
#===================================================================
# checksum generation
@@ -629,6 +642,18 @@ class GenericHandler(MinimalHandler):
@classmethod
def hash(cls, secret, **kwds):
+ if kwds:
+ # Deprecating passing any settings keywords via .hash() as of passlib 1.7; everything
+ # should use .replace().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:
+ # TODO: uncomment this ones UTs are adjusted to expect warning...
+ # warn_hash_settings_deprecation(cls, settings)
+ return cls.replace(**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)
@@ -652,10 +677,14 @@ class GenericHandler(MinimalHandler):
@deprecated_method(deprecated="1.7", removed="2.0")
@classmethod
- def genconfig(cls, **settings):
+ 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.replace(**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, **settings)
+ self = cls(use_defaults=True, **kwds)
self.checksum = self._stub_checksum
return self.to_string()
@@ -1169,6 +1198,7 @@ class HasSalt(GenericHandler):
def replace(cls, # keyword only...
default_salt_size=None,
salt_size=None, # aliases used by CryptContext
+ salt=None,
**kwds):
# check for aliases used by CryptContext
@@ -1181,14 +1211,19 @@ class HasSalt(GenericHandler):
subcls = super(HasSalt, cls).replace(**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=kwds.get("relaxed"))
+ relaxed=relaxed)
- return subcls
+ # 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)
# XXX: would like to combine w/ _norm_salt() code below, but doesn't quite fit.
@classmethod
@@ -1243,21 +1278,30 @@ class HasSalt(GenericHandler):
#===================================================================
# init
#===================================================================
- def __init__(self, salt=None, salt_size=None, **kwds):
+ def __init__(self, salt=None, **kwds):
super(HasSalt, self).__init__(**kwds)
- self.salt = self._norm_salt(salt, salt_size=salt_size)
+ 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
- def _norm_salt(self, salt, salt_size=None):
- """helper to normalize & validate user-provided salt string
+ # NOTE: split out mainly so sha256_crypt can subclass this
+ def _parse_salt(self, salt):
+ return self._norm_salt(salt)
- If no salt provided, a random salt is generated
- using :attr:`default_salt_size` and :attr:`default_salt_chars`.
+ @classmethod
+ def _norm_salt(cls, salt, relaxed=False):
+ """helper to normalize & validate user-provided salt string
- :arg salt: salt string or ``None``
- :param salt_size: optionally specified size of autogenerated salt
+ :arg salt:
+ salt string
:raises TypeError:
- If salt not provided and ``use_defaults=False``.
+ If salt not correct type.
:raises ValueError:
@@ -1268,49 +1312,40 @@ class HasSalt(GenericHandler):
and a warning is issued instead).
:returns:
- normalized or generated salt
+ normalized salt
"""
- # generate new salt if none provided
- if salt is None:
- if not self.use_defaults:
- raise TypeError("no salt specified")
- if salt_size is None:
- salt_size = self.default_salt_size
- salt = self._generate_salt(salt_size)
-
# check type
- if self._salt_is_bytes:
+ if 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 self.relaxed):
+ if isinstance(salt, bytes) and (PY2 or relaxed):
salt = salt.decode("ascii")
else:
raise exc.ExpectedTypeError(salt, "unicode", "salt")
# check charset
- sc = self.salt_chars
+ 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" % self.name)
+ raise ValueError("invalid characters in %s salt" % cls.name)
# check min size
- mn = self.min_salt_size
+ mn = cls.min_salt_size
if mn and len(salt) < mn:
- msg = "salt too small (%s requires %s %d %s)" % (self.name,
- "exactly" if mn == self.max_salt_size else ">=", mn,
- self._salt_unit)
+ 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 = self.max_salt_size
+ mx = cls.max_salt_size
if mx and len(salt) > mx:
- msg = "salt too large (%s requires %s %d %s)" % (self.name,
- "exactly" if mx == mn else "<=", mx, self._salt_unit)
- if self.relaxed:
+ 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 = self._truncate_salt(salt, mx)
+ salt = cls._truncate_salt(salt, mx)
else:
raise ValueError(msg)
@@ -1323,12 +1358,12 @@ class HasSalt(GenericHandler):
# the truncation properly
return salt[:mx]
- def _generate_salt(self, salt_size):
- """helper method for _norm_salt(); generates a new random salt string.
-
- :arg salt_size: salt size to generate
+ @classmethod
+ def _generate_salt(cls):
"""
- return getrandstr(rng, self.default_salt_chars, salt_size)
+ 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):
@@ -1362,9 +1397,10 @@ class HasRawSalt(HasSalt):
_salt_is_bytes = True
_salt_unit = "bytes"
- def _generate_salt(self, salt_size):
- assert self.salt_chars in [None, ALL_BYTE_VALUES]
- return getrandbytes(rng, salt_size)
+ @classmethod
+ def _generate_salt(cls):
+ assert cls.salt_chars in [None, ALL_BYTE_VALUES]
+ return getrandbytes(rng, cls.default_salt_size)
#------------------------------------------------------------------------
# rounds mixin
@@ -1572,44 +1608,6 @@ class HasRounds(GenericHandler):
return subcls
@classmethod
- def _clip_to_valid_rounds(cls, rounds, param="rounds", relaxed=True):
- """
- internal helper --
- clip rounds value to handler's absolute limits (min_rounds / max_rounds)
-
- :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
- """
- # check minimum
- mn = cls.min_rounds
- if rounds < mn:
- msg = "%s: %s (%r) below min_rounds (%d)" % (cls.name, param, rounds, mn)
- if relaxed:
- warn(msg, PasslibHashWarning)
- rounds = mn
- else:
- raise ValueError(msg)
-
- # check maximum
- mx = cls.max_rounds
- if mx and rounds > mx:
- msg = "%s: %s (%r) above max_rounds (%d)" % (cls.name, param, rounds, mx)
- if relaxed:
- warn(msg, PasslibHashWarning)
- rounds = mx
- else:
- raise ValueError(msg)
-
- return rounds
-
- @classmethod
def _clip_to_desired_rounds(cls, rounds):
"""
helper for :meth:`_generate_rounds` --
@@ -1673,13 +1671,33 @@ class HasRounds(GenericHandler):
#===================================================================
def __init__(self, rounds=None, **kwds):
super(HasRounds, self).__init__(**kwds)
- self.rounds = self._norm_rounds(rounds)
+ 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
- def _norm_rounds(self, 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: ``None``, or an integer cost parameter.
+ :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
@@ -1696,55 +1714,48 @@ class HasRounds(GenericHandler):
:returns:
normalized rounds value
"""
-
- # init rounds attr, using default_rounds (etc) if needed
- explicit = False
- if rounds is None:
- if not self.use_defaults:
- raise TypeError("no rounds specified")
- rounds = self._generate_rounds() # NOTE: will throw ValueError if default not set
- assert isinstance(rounds, int_types)
- elif self.use_defaults:
- # warn if rounds is outside desired bounds only if user provided explicit rounds
- # to .hash() -- hence the .use_defaults check, which will be false if we're
- # coming from .verify() / .genhash()
- explicit = True
-
# check type
if not isinstance(rounds, int_types):
- raise exc.ExpectedTypeError(rounds, "integer", "rounds")
-
- # check valid bounds
- rounds = self._clip_to_valid_rounds(rounds, relaxed=self.relaxed)
-
- # if rounds explicitly specified, warn if outside desired bounds, but use it
- if explicit:
- mnd = self.min_desired_rounds
- if mnd and rounds < mnd:
- warn("using rounds value (%r) below desired minimum (%d)" % (rounds, mnd),
- exc.PasslibConfigWarning)
-
- mxd = self.max_desired_rounds
- if mxd and rounds > mxd:
- warn("using rounds value (%r) above desired maximum (%d)" % (rounds, mxd),
- exc.PasslibConfigWarning)
+ raise exc.ExpectedTypeError(rounds, "integer", param)
+
+ # check minimum
+ mn = cls.min_rounds
+ if rounds < mn:
+ msg = "%s: %s (%r) below min_rounds (%d)" % (cls.name, param, rounds, mn)
+ if relaxed:
+ warn(msg, PasslibHashWarning)
+ rounds = mn
+ else:
+ raise ValueError(msg)
+
+ # check maximum
+ mx = cls.max_rounds
+ if mx and rounds > mx:
+ msg = "%s: %s (%r) above max_rounds (%d)" % (cls.name, param, rounds, mx)
+ if relaxed:
+ warn(msg, PasslibHashWarning)
+ rounds = mx
+ else:
+ raise ValueError(msg)
+
return rounds
- def _generate_rounds(self):
+ @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 = self.default_rounds
+ rounds = cls.default_rounds
if rounds is None:
- raise TypeError("%s rounds value must be specified explicitly" % (self.name,))
+ 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 self.vary_rounds:
- lower, upper = self._calc_vary_rounds_range(rounds)
+ if cls.vary_rounds:
+ lower, upper = cls._calc_vary_rounds_range(rounds)
assert lower <= rounds <= upper
if lower < upper:
rounds = rng.randint(lower, upper)