summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-05-03 15:14:45 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-05-03 15:14:45 -0400
commitf58865b964e4df6f98bc93700f9ea9dc596cca17 (patch)
treeae42228660d1befb53bd9bd320935420948f65dd
parent80f60261f698fe3d147bd9b0ff2ff3e0f1cef732 (diff)
downloadpasslib-f58865b964e4df6f98bc93700f9ea9dc596cca17.tar.gz
tightened salt info specifications; improved salt info conformance tests
-rw-r--r--CHANGES8
-rw-r--r--docs/lib/passlib.utils.rst4
-rw-r--r--docs/password_hash_api.rst72
-rw-r--r--passlib/handlers/md5_crypt.py4
-rw-r--r--passlib/handlers/sha2_crypt.py4
-rw-r--r--passlib/handlers/sun_md5_crypt.py12
-rw-r--r--passlib/tests/test_utils_handlers.py32
-rw-r--r--passlib/tests/utils.py165
-rw-r--r--passlib/utils/__init__.py10
-rw-r--r--passlib/utils/handlers.py2
10 files changed, 211 insertions, 102 deletions
diff --git a/CHANGES b/CHANGES
index 6d34f3c..bb9725f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -18,8 +18,7 @@ Release History
* added support for all hashes used by the Roundup Issue Tracker
* bsdi_crypt, sha1_crypt now check for OS crypt() support
* ``salt_size`` keyword added to encrypt() method of all
- the hashes which support variable-length salts
- (cheifly: pbkdf2_{digest}, sha1_crypt, grub_pbkdf2_sha512).
+ the hashes which support variable-length salts.
* security fix: disabled unix_fallback's "wildcard password" support
unless explicitly enabled by user.
@@ -60,6 +59,11 @@ Release History
- renamed *salt_charset* -> *salt_chars*
- old attributes still present, but deprecated - will remove in 1.5
+ * password hash api - tightened specifications for salt & rounds parameters,
+ added support for hashes w/ no max salt size.
+
+ * improved password hash api conformance tests
+
**1.3.1** (2011-03-28)
* bugfix: replaced "sys.maxsize" reference that was failing under py25
diff --git a/docs/lib/passlib.utils.rst b/docs/lib/passlib.utils.rst
index 370614f..1fa826f 100644
--- a/docs/lib/passlib.utils.rst
+++ b/docs/lib/passlib.utils.rst
@@ -54,6 +54,10 @@ Object Tests
.. autofunction:: is_crypt_context
+.. autofunction:: has_rounds_info
+
+.. autofunction:: has_salt_info
+
Submodules
==========
There are also a few sub modules which provide additional utility functions:
diff --git a/docs/password_hash_api.rst b/docs/password_hash_api.rst
index 09c4505..609b8c2 100644
--- a/docs/password_hash_api.rst
+++ b/docs/password_hash_api.rst
@@ -1,6 +1,6 @@
.. index::
single: password hash api
- pair: custom hash handler; requirements
+ single: custom hash handler; requirements
.. currentmodule:: passlib.hash
@@ -120,6 +120,12 @@ Required Attributes
a specific salt string; though not only is this far from needed
for most cases, the salt string's content constraints vary for each algorithm.
+ ``salt_size``
+ If present, this means the algorithm will auto-generate a salt
+ of the specified number of characters
+ (assuming ``salt`` is not specified explicitly).
+ If omitted, most algorithms will fall back to a default salt size.
+
``rounds``
If present, this means the algorithm allows for a variable number of rounds
to be used, allowing the processor time required to be increased.
@@ -361,28 +367,33 @@ across all handlers in passlib.
Consider making these attributes required for all hashes
which support the appropriate :attr:`settings` keyword.
+.. _optional-rounds-attributes:
+
Rounds Information
------------------
For schemes which support a variable number of rounds (ie, ``'rounds' in PasswordHash.setting_kwds``),
the following attributes are usually exposed.
-(Applications can test for this suites' presence by checking if ``getattr(handler,"max_rounds",None)>0``)
+(Applications can test for this suites' presence by using :func:`~passlib.utils.has_rounds_info`)
-.. attribute:: PasswordHash.default_rounds
+.. attribute:: PasswordHash.max_rounds
- The default number of rounds that will be used if not
- explicitly set when calling :meth:`~PasswordHash.encrypt` or :meth:`~PasswordHash.genconfig`.
+ The maximum number of rounds the scheme allows.
+ Specifying values above this will generally result
+ in a warning, and :attr:`~!PasswordHash.max_rounds` will be used instead.
+ Must be a positive integer.
.. attribute:: PasswordHash.min_rounds
The minimum number of rounds the scheme allows.
Specifying values below this will generally result
in a warning, and :attr:`~!PasswordHash.min_rounds` will be used instead.
+ Must be within ``range(0, max_rounds+1)``.
-.. attribute:: PasswordHash.max_rounds
+.. attribute:: PasswordHash.default_rounds
- The maximum number of rounds the scheme allows.
- Specifying values above this will generally result
- in a warning, and :attr:`~!PasswordHash.max_rounds` will be used instead.
+ The default number of rounds that will be used if not
+ explicitly set when calling :meth:`~PasswordHash.encrypt` or :meth:`~PasswordHash.genconfig`.
+ Must be within ``range(min_rounds, max_rounds+1)``.
.. attribute:: PasswordHash.rounds_cost
@@ -395,24 +406,38 @@ the following attributes are usually exposed.
``log2``
time taken scales exponentially with rounds value (eg: :class:`~passlib.hash.bcrypt`)
+.. _optional-salt-attributes:
+
Salt Information
----------------
For schemes which support a salt (ie, ``'salt' in PasswordHash.setting_kwds``),
the following attributes are usually exposed.
-(Applications can test for this suites' presence by checking if ``getattr(handler,"max_salt_size",None)>0``)
+(Applications can test for this suites' presence by using :func:`~passlib.utils.has_salt_info`)
.. attribute:: PasswordHash.max_salt_size
- maximum number of characters which will be *used*
+ maximum number of characters which will be used
if a salt string is provided to :meth:`~PasswordHash.genconfig` or :meth:`~PasswordHash.encrypt`.
- must be positive integer if salts are supported,
- may be ``None`` or ``0`` if salts are not supported.
+ must be one of:
+
+ * A positive integer - it should accept and silently truncate
+ any salt strings longer than this size.
+
+ * ``None`` - the scheme should use all characters of a provided salt,
+ no matter how large.
.. attribute:: PasswordHash.min_salt_size
minimum number of characters required in salt string,
if provided to :meth:`~PasswordHash.genconfig` or :meth:`~PasswordHash.encrypt`.
- must be non-negative integer that is not greater than :attr:`~PasswordHash.max_salt_size`.
+ must be an integer within ``range(0,max_salt_size+1)``.
+
+.. attribute:: PasswordHash.default_salt_size
+
+ size of salts generated by genconfig
+ when no salt is provided by caller.
+ for most hashes, this defaults to :attr:`!PasswordHash.max_salt_size`.
+ this value must be within ``range(min_salt_size, max_salt_size+1)``.
.. attribute:: PasswordHash.salt_chars
@@ -431,21 +456,15 @@ the following attributes are usually exposed.
.. todo::
this list the behavior for handlers which accept
- an salt string containing characters.
- some handlers take raw bytes
- for their salt keyword, how these attributes change
- in that situtation should be documentated.
-
+ salt strings containing encoded characters.
+ some handlers instead take raw bytes for their salt keyword,
+ and handle encoding / decoding them internally.
+ it should be documented how these attributes
+ behave in that situation.
..
not yet documentated, want to make sure this is how we want to do things:
- .. attribute:: PasswordHash.default_salt_size
-
- size of salts generated by genconfig
- when no salt is provided by caller.
- for most hashes, this defaults to :attr:`!PasswordHash.max_salt_size`.
-
.. attribute:: PasswordHash.default_salt_chars
sequence of characters used to generated new salts
@@ -456,6 +475,9 @@ the following attributes are usually exposed.
the full range to be accepted, while only
a select subset to be used for generation.
+ xxx: what about a bits_per_salt_char or some such, so effective salt strength
+ can be compared?
+
Footnotes
=========
.. [#otypes] While this specification is written referring to classes and classmethods,
diff --git a/passlib/handlers/md5_crypt.py b/passlib/handlers/md5_crypt.py
index 557f0a8..a4deb72 100644
--- a/passlib/handlers/md5_crypt.py
+++ b/passlib/handlers/md5_crypt.py
@@ -149,7 +149,7 @@ class md5_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
#=========================================================
#--GenericHandler--
name = "md5_crypt"
- setting_kwds = ("salt",)
+ setting_kwds = ("salt", "salt_size")
ident = "$1$"
checksum_size = 22
@@ -217,7 +217,7 @@ class apr_md5_crypt(uh.HasSalt, uh.GenericHandler):
#=========================================================
#--GenericHandler--
name = "apr_md5_crypt"
- setting_kwds = ("salt",)
+ setting_kwds = ("salt", "salt_size")
ident = "$apr1$"
checksum_size = 22
diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py
index eb8f6b6..56e72e9 100644
--- a/passlib/handlers/sha2_crypt.py
+++ b/passlib/handlers/sha2_crypt.py
@@ -241,7 +241,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#=========================================================
#--GenericHandler--
name = "sha256_crypt"
- setting_kwds = ("salt", "rounds", "implicit_rounds")
+ setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")
ident = "$5$"
#--HasSalt--
@@ -388,7 +388,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
name = "sha512_crypt"
ident = "$6$"
- setting_kwds = ("salt", "rounds", "implicit_rounds")
+ setting_kwds = ("salt", "rounds", "implicit_rounds", "salt_size")
min_salt_size = 0
max_salt_size = 16
diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py
index dfa5559..4306b5c 100644
--- a/passlib/handlers/sun_md5_crypt.py
+++ b/passlib/handlers/sun_md5_crypt.py
@@ -174,9 +174,14 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
:param salt:
Optional salt string.
- If not specified, an 8 character salt will be autogenerated (this is recommended).
+ If not specified, a salt will be autogenerated (this is recommended).
If specified, it must be drawn from the regexp range ``[./0-9A-Za-z]``.
+ :param salt_size:
+ If no salt is specified, this parameter can be used to specify
+ the size (in characters) of the autogenerated salt.
+ It currently defaults to 8.
+
:param rounds:
Optional number of rounds to use.
Defaults to 5000, must be between 0 and 4294963199, inclusive.
@@ -192,17 +197,16 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#class attrs
#=========================================================
name = "sun_md5_crypt"
- setting_kwds = ("salt", "rounds", "bare_salt")
+ setting_kwds = ("salt", "rounds", "bare_salt", "salt_size")
#NOTE: docs say max password length is 255.
#release 9u2
#NOTE: not sure if original crypt has a salt size limit,
# all instances that have been seen use 8 chars.
- # setting max+strict bounds just to keep inputs sane.
default_salt_size = 8
min_salt_size = 0
- max_salt_size = 32
+ max_salt_size = None
salt_chars = uh.H64_CHARS
default_rounds = 5000 #current passlib default
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index 54487ed..c4a8f01 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -101,7 +101,7 @@ class SkeletonTest(TestCase):
self.assertRaises(ValueError, d1.norm_checksum, 'xxxxx')
self.assertRaises(ValueError, d1.norm_checksum, 'xxyx')
- def test_12_norm_salt(self):
+ def test_20_norm_salt(self):
"test GenericHandler+HasSalt: .norm_salt(), .generate_salt()"
class d1(uh.HasSalt, uh.GenericHandler):
name = 'd1'
@@ -129,7 +129,31 @@ class SkeletonTest(TestCase):
self.assertEquals(len(d1.norm_salt(None,salt_size=5)), 3)
self.assertRaises(ValueError, d1.norm_salt, None, salt_size=5, strict=True)
- def test_13_norm_rounds(self):
+ def test_21_norm_salt(self):
+ "test GenericHandler+HasSalt: .norm_salt(), .generate_salt() - with no max_salt_size"
+ class d1(uh.HasSalt, uh.GenericHandler):
+ name = 'd1'
+ setting_kwds = ('salt',)
+ min_salt_size = 1
+ max_salt_size = None
+ default_salt_size = 2
+ salt_chars = 'a'
+
+ #check salt=None
+ self.assertEqual(d1.norm_salt(None), 'aa')
+ self.assertRaises(ValueError, d1.norm_salt, None, strict=True)
+
+ #check small & large salts
+ self.assertRaises(ValueError, d1.norm_salt, '')
+ self.assertEqual(d1.norm_salt('aaaa', strict=True), 'aaaa')
+
+ #check generate salt (indirectly)
+ self.assertEquals(len(d1.norm_salt(None)), 2)
+ self.assertEquals(len(d1.norm_salt(None,salt_size=1)), 1)
+ self.assertEquals(len(d1.norm_salt(None,salt_size=3)), 3)
+ self.assertEquals(len(d1.norm_salt(None,salt_size=5)), 5)
+
+ def test_30_norm_rounds(self):
"test GenericHandler+HasRounds: .norm_rounds()"
class d1(uh.HasRounds, uh.GenericHandler):
name = 'd1'
@@ -154,7 +178,7 @@ class SkeletonTest(TestCase):
d1.default_rounds = None
self.assertRaises(ValueError, d1.norm_rounds, None)
- def test_14_backends(self):
+ def test_40_backends(self):
"test GenericHandler+HasManyBackends"
class d1(uh.HasManyBackends, uh.GenericHandler):
name = 'd1'
@@ -198,7 +222,7 @@ class SkeletonTest(TestCase):
d1.set_backend('a')
self.assertEquals(obj.calc_checksum('s'), 'a')
- def test_15_bh_norm_ident(self):
+ def test_50_bh_norm_ident(self):
"test GenericHandler+HasManyIdents: .norm_ident() & .identify()"
class d1(uh.HasManyIdents, uh.GenericHandler):
name = 'd1'
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index f4c9269..20197c2 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -10,6 +10,7 @@ import os
import tempfile
import unittest
import warnings
+from warnings import warn
try:
from warnings import catch_warnings
except ImportError:
@@ -25,7 +26,9 @@ except ImportError:
from nose.plugins.skip import SkipTest
#pkg
from passlib import registry
-from passlib.utils import classproperty, handlers as uh
+from passlib.utils import classproperty, handlers as uh, \
+ has_rounds_info, has_salt_info, \
+ rounds_cost_values
#local
__all__ = [
#util funcs
@@ -305,14 +308,6 @@ class HandlerCase(TestCase):
name += " (%s backend)" % (backend(),)
return name
- @classproperty
- def has_salt_info(cls):
- return 'salt' in cls.handler.setting_kwds and getattr(cls.handler, "max_salt_size", None) > 0
-
- @classproperty
- def has_rounds_info(cls):
- return 'rounds' in cls.handler.setting_kwds and getattr(cls.handler, "max_rounds", None) > 0
-
backend = "default"
def setUp(self):
@@ -348,51 +343,80 @@ class HandlerCase(TestCase):
self.assert_(context is not None, "context_kwds must be defined:")
self.assertIsInstance(context, tuple, "context_kwds must be a tuple:")
- #TODO: check optional rounds attributes & salt attributes
-
- def test_05_ext_handler(self):
- "check configuration of GenericHandler-derived classes"
+ def test_01_optional_salt_attributes(self):
+ "validate optional salt attributes"
cls = self.handler
- if not isinstance(cls, type) or not issubclass(cls, uh.GenericHandler):
+ if not has_salt_info(cls):
raise SkipTest
- if 'salt' in cls.setting_kwds:
- # assume HasSalt / HasRawSalt
-
- if cls.min_salt_size > cls.max_salt_size:
- raise AssertionError("min salt chars too large")
-
- if cls.default_salt_size < cls.min_salt_size:
- raise AssertionError("default salt chars too small")
- if cls.default_salt_size > cls.max_salt_size:
- raise AssertionError("default salt chars too large")
-
- if cls.salt_chars:
- if not cls.default_salt_chars:
- raise AssertionError("default salt charset must not be empty")
- if any(c not in cls.salt_chars for c in cls.default_salt_chars):
- raise AssertionError("default salt charset not subset of salt charset: %r" % (c,))
- else:
- if not cls.default_salt_chars:
- raise AssertionError("default salt charset must be specified if salt_chars is empty")
-
-
- if 'rounds' in cls.setting_kwds:
- # assume uses HasRounds
- if cls.max_rounds is None:
- raise AssertionError("max rounds not specified")
+ #check max_salt_size
+ mx_set = (cls.max_salt_size is not None)
+ if mx_set and cls.max_salt_size < 1:
+ raise AssertionError("max_salt_chars must be >= 1")
+
+ #check min_salt_size
+ if cls.min_salt_size < 0:
+ raise AssertionError("min_salt_chars must be >= 0")
+ if mx_set and cls.min_salt_size > cls.max_salt_size:
+ raise AssertionError("min_salt_chars must be <= max_salt_chars")
+
+ #check default_salt_size
+ if cls.default_salt_size < cls.min_salt_size:
+ raise AssertionError("default_salt_size must be >= min_salt_size")
+ if mx_set and cls.default_salt_size > cls.max_salt_size:
+ raise AssertionError("default_salt_size must be <= max_salt_size")
+
+ #check for 'salt_size' keyword
+ if 'salt_size' not in cls.setting_kwds and \
+ (not mx_set or cls.min_salt_size < cls.max_salt_size):
+ #NOTE: for now, only bothering to issue warning if default_salt_size isn't maxed out
+ if (not mx_set or cls.default_salt_size < cls.max_salt_size):
+ warn("%s: hash handler supports range of salt sizes, but doesn't specify 'salt_size' setting" % (cls.name,))
+
+ #check salt_chars & default_salt_chars
+ if cls.salt_chars:
+ if not cls.default_salt_chars:
+ raise AssertionError("default_salt_chars must not be empty")
+ if any(c not in cls.salt_chars for c in cls.default_salt_chars):
+ raise AssertionError("default_salt_chars must be subset of salt_chars: %r not in salt_chars" % (c,))
+ else:
+ if not cls.default_salt_chars:
+ raise AssertionError("default_salt_chars MUST be specified if salt_chars is empty")
- if cls.min_rounds > cls.max_rounds:
- raise AssertionError("min rounds too large")
+ def test_02_optional_rounds_attributes(self):
+ "validate optional rounds attributes"
+ cls = self.handler
+ if not has_rounds_info(cls):
+ raise SkipTest
- if cls.default_rounds is not None:
- if cls.default_rounds < cls.min_rounds:
- raise AssertionError("default rounds too small")
- if cls.default_rounds > cls.max_rounds:
- raise AssertionError("default rounds too large")
+ #check max_rounds
+ if cls.max_rounds is None:
+ raise AssertionError("max_rounds not specified")
+ if cls.max_rounds < 1:
+ raise AssertionError("max_rounds must be >= 1")
+
+ #check min_rounds
+ if cls.min_rounds < 0:
+ raise AssertionError("min_rounds must be >= 0")
+ if cls.min_rounds > cls.max_rounds:
+ raise AssertionError("min_rounds must be <= max_rounds")
+
+ #check default_rounds
+ if cls.default_rounds is not None:
+ if cls.default_rounds < cls.min_rounds:
+ raise AssertionError("default_rounds must be >= min_rounds")
+ if cls.default_rounds > cls.max_rounds:
+ raise AssertionError("default_rounds must be <= max_rounds")
+
+ #check rounds_cost
+ if cls.rounds_cost not in rounds_cost_values:
+ raise AssertionError("unknown rounds cost constant: %r" % (cls.rounds_cost,))
- if cls.rounds_cost not in ("linear", "log2"):
- raise AssertionError("unknown rounds cost function")
+ def test_05_ext_handler(self):
+ "check configuration of GenericHandler-derived classes"
+ cls = self.handler
+ if not isinstance(cls, type) or not issubclass(cls, uh.GenericHandler):
+ raise SkipTest
if 'ident' in cls.setting_kwds:
# assume uses HasManyIdents
@@ -528,9 +552,9 @@ class HandlerCase(TestCase):
def test_31_genconfig_minsalt(self):
"test genconfig() honors min salt chars"
- if not self.has_salt_info:
- raise SkipTest
handler = self.handler
+ if not has_salt_info(handler):
+ raise SkipTest
cs = handler.salt_chars
mn = handler.min_salt_size
c1 = self.do_genconfig(salt=cs[0] * mn)
@@ -539,35 +563,52 @@ class HandlerCase(TestCase):
def test_32_genconfig_maxsalt(self):
"test genconfig() honors max salt chars"
- if not self.has_salt_info:
- raise SkipTest
handler = self.handler
+ if not has_salt_info(handler):
+ raise SkipTest
cs = handler.salt_chars
mx = handler.max_salt_size
- c1 = self.do_genconfig(salt=cs[0] * mx)
- c2 = self.do_genconfig(salt=cs[0] * (mx+1))
- self.assertEquals(c1,c2)
+ if mx is None:
+ #make sure salt is NOT truncated,
+ #use a really large salt for testing
+ salt = cs[0] * 1024
+ c1 = self.do_genconfig(salt=salt)
+ c2 = self.do_genconfig(salt=salt + cs[0])
+ self.assertNotEqual(c1,c2)
+ else:
+ #make sure salt is truncated exactly where it should be.
+ salt = cs[0] * mx
+ c1 = self.do_genconfig(salt=salt)
+ c2 = self.do_genconfig(salt=salt + cs[0])
+ self.assertEqual(c1,c2)
+
+ #if min_salt supports it, check smaller than mx is NOT truncated
+ if handler.min_salt_size < mx:
+ c3 = self.do_genconfig(salt=salt[:-1])
+ self.assertNotEqual(c1,c3)
def test_33_genconfig_saltcharset(self):
"test genconfig() honors salt charset"
- if not self.has_salt_info:
- raise SkipTest
handler = self.handler
+ if not has_salt_info(handler):
+ raise SkipTest
mx = handler.max_salt_size
mn = handler.min_salt_size
cs = handler.salt_chars
#make sure all listed chars are accepted
- for i in xrange(0,len(cs),mx):
- salt = cs[i:i+mx]
+ chunk = 1024 if mx is None else mx
+ for i in xrange(0,len(cs),chunk):
+ salt = cs[i:i+chunk]
if len(salt) < mn:
- salt = (salt*(mn//len(salt)+1))[:mx]
+ salt = (salt*(mn//len(salt)+1))[:chunk]
self.do_genconfig(salt=salt)
- #check some invalid salt chars are rejected
+ #check some invalid salt chars, make sure they're rejected
+ chunk = mn if mn > 0 else 1
for c in '\x00\xff':
if c not in cs:
- self.assertRaises(ValueError, self.do_genconfig, salt=c*mx)
+ self.assertRaises(ValueError, self.do_genconfig, salt=c*chunk)
#=========================================================
#genhash()
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 23365e3..9aa72b6 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -62,6 +62,8 @@ unix_crypt_schemes = [
"bsdi_crypt", "des_crypt"
]
+#: list of rounds_cost constants
+rounds_cost_values = [ "linear", "log2" ]
#: special byte string containing all possible byte values, used in a few places.
#XXX: treated as singleton by some of the code for efficiency.
@@ -169,6 +171,14 @@ def is_crypt_context(obj):
"verify", "encrypt", "identify",
))
+def has_rounds_info(handler):
+ "check if handler provides the optional :ref:`rounds information <optional-rounds-attributes>` attributes"
+ return 'rounds' in handler.setting_kwds and getattr(handler, "min_rounds", None) is not None
+
+def has_salt_info(handler):
+ "check if handler provides the optional :ref:`salt information <optional-salt-attributes>` attributes"
+ return 'salt' in handler.setting_kwds and getattr(handler, "min_salt_size", None) is not None
+
#=================================================================================
#string helpers
#=================================================================================
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index fabe63a..aba9af5 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -1262,7 +1262,7 @@ class HasSalt(GenericHandler):
#check max size
mx = cls.max_salt_size
- if len(salt) > mx:
+ if mx is not None and len(salt) > mx:
if strict:
raise ValueError("%s salt string must be at most %d characters" % (cls.name, mx))
salt = salt[:mx]