Преглед изворни кода

Bump version of pypbkdf2 from 1.2 to 1.3

Oz N Tiram пре 8 година
родитељ
комит
ee8a261b61
1 измењених фајлова са 222 додато и 165 уклоњено
  1. 222 165
      pwman/util/crypto/pypbkdf2.py

+ 222 - 165
pwman/util/crypto/pypbkdf2.py

@@ -1,34 +1,35 @@
 #!/usr/bin/python
 #!/usr/bin/python
 # -*- coding: ascii -*-
 # -*- coding: ascii -*-
 ###########################################################################
 ###########################################################################
-# PBKDF2.py - PKCS#5 v2.0 Password-Based Key Derivation
+# pbkdf2 - PKCS#5 v2.0 Password-Based Key Derivation
 #
 #
-# Copyright (C) 2007, 2008 Dwayne C. Litzenberger <dlitz@dlitz.net>
-# All rights reserved.
-# 
-# Permission to use, copy, modify, and distribute this software and its
-# documentation for any purpose and without fee is hereby granted,
-# provided that the above copyright notice appear in all copies and that
-# both that copyright notice and this permission notice appear in
-# supporting documentation.
-# 
-# THE AUTHOR PROVIDES THIS SOFTWARE ``AS IS'' AND ANY EXPRESSED OR 
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
-# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  
-# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
-# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# Copyright (C) 2007-2011 Dwayne C. Litzenberger <dlitz@dlitz.net>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 #
 #
 # Country of origin: Canada
 # Country of origin: Canada
 #
 #
 ###########################################################################
 ###########################################################################
 # Sample PBKDF2 usage:
 # Sample PBKDF2 usage:
 #   from Crypto.Cipher import AES
 #   from Crypto.Cipher import AES
-#   from PBKDF2 import PBKDF2
+#   from pbkdf2 import PBKDF2
 #   import os
 #   import os
 #
 #
 #   salt = os.urandom(8)    # 64-bit salt
 #   salt = os.urandom(8)    # 64-bit salt
@@ -38,7 +39,7 @@
 #     ...
 #     ...
 #
 #
 # Sample crypt() usage:
 # Sample crypt() usage:
-#   from PBKDF2 import crypt
+#   from pbkdf2 import crypt
 #   pwhash = crypt("secret")
 #   pwhash = crypt("secret")
 #   alleged_pw = raw_input("Enter password: ")
 #   alleged_pw = raw_input("Enter password: ")
 #   if pwhash == crypt(alleged_pw, pwhash):
 #   if pwhash == crypt(alleged_pw, pwhash):
@@ -47,54 +48,74 @@
 #       print "Invalid password"
 #       print "Invalid password"
 #
 #
 ###########################################################################
 ###########################################################################
-# History:
-#
-#  2007-07-27 Dwayne C. Litzenberger <dlitz@dlitz.net>
-#   - Initial Release (v1.0)
-#
-#  2007-07-31 Dwayne C. Litzenberger <dlitz@dlitz.net>
-#   - Bugfix release (v1.1)
-#   - SECURITY: The PyCrypto XOR cipher (used, if available, in the _strxor
-#   function in the previous release) silently truncates all keys to 64
-#   bytes.  The way it was used in the previous release, this would only be
-#   problem if the pseudorandom function that returned values larger than
-#   64 bytes (so SHA1, SHA256 and SHA512 are fine), but I don't like
-#   anything that silently reduces the security margin from what is
-#   expected.
-#  
-# 2008-06-17 Dwayne C. Litzenberger <dlitz@dlitz.net>
-#   - Compatibility release (v1.2)
-#   - Add support for older versions of Python (2.2 and 2.3).
-#
-###########################################################################
-
-__version__ = "1.2"
+__version__ = "1.3"
+__all__ = ['PBKDF2', 'crypt']
 
 
 from struct import pack
 from struct import pack
-from binascii import b2a_hex
 from random import randint
 from random import randint
 import string
 import string
-import collections
+import sys
 
 
 try:
 try:
-    # Use PyCrypto (if available)
+    # Use PyCrypto (if available).
     from Crypto.Hash import HMAC, SHA as SHA1
     from Crypto.Hash import HMAC, SHA as SHA1
-
 except ImportError:
 except ImportError:
     # PyCrypto not available.  Use the Python standard library.
     # PyCrypto not available.  Use the Python standard library.
     import hmac as HMAC
     import hmac as HMAC
-    import sha as SHA1
+    try:
+        from hashlib import sha1 as SHA1
+    except ImportError:
+        # hashlib not available.  Use the old sha module.
+        import sha as SHA1
 
 
-def strxor(a, b):
-    return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])
+#
+# Python 2.1 thru 3.2 compatibility
+#
 
 
-def b64encode(data, chars="+/"):
-    tt = string.maketrans("+/", chars)
-    return data.encode('base64').replace("\n", "").translate(tt)
+if sys.version_info[0] == 2:
+    _0xffffffffL = long(1) << 32
+    def isunicode(s):
+        return isinstance(s, unicode)
+    def isbytes(s):
+        return isinstance(s, str)
+    def isinteger(n):
+        return isinstance(n, (int, long))
+    def b(s):
+        return s
+    def binxor(a, b):
+        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])
+    def b64encode(data, chars="+/"):
+        tt = string.maketrans("+/", chars)
+        return data.encode('base64').replace("\n", "").translate(tt)
+    from binascii import b2a_hex
+else:
+    _0xffffffffL = 0xffffffff
+    def isunicode(s):
+        return isinstance(s, str)
+    def isbytes(s):
+        return isinstance(s, bytes)
+    def isinteger(n):
+        return isinstance(n, int)
+    def callable(obj):
+        return hasattr(obj, '__call__')
+    def b(s):
+       return s.encode("latin-1")
+    def binxor(a, b):
+        return bytes([x ^ y for (x, y) in zip(a, b)])
+    from base64 import b64encode as _b64encode
+    def b64encode(data, chars="+/"):
+        if isunicode(chars):
+            return _b64encode(data, chars.encode('utf-8')).decode('utf-8')
+        else:
+            return _b64encode(data, chars)
+    from binascii import b2a_hex as _b2a_hex
+    def b2a_hex(s):
+        return _b2a_hex(s).decode('us-ascii')
+    xrange = range
 
 
 class PBKDF2(object):
 class PBKDF2(object):
     """PBKDF2.py : PKCS#5 v2.0 Password-Based Key Derivation
     """PBKDF2.py : PKCS#5 v2.0 Password-Based Key Derivation
-    
+
     This implementation takes a passphrase and a salt (and optionally an
     This implementation takes a passphrase and a salt (and optionally an
     iteration count, a digest module, and a MAC module) and provides a
     iteration count, a digest module, and a MAC module) and provides a
     file-like object from which an arbitrarily-sized key can be read.
     file-like object from which an arbitrarily-sized key can be read.
@@ -104,10 +125,10 @@ class PBKDF2(object):
 
 
     The idea behind PBKDF2 is to derive a cryptographic key from a
     The idea behind PBKDF2 is to derive a cryptographic key from a
     passphrase and a salt.
     passphrase and a salt.
-    
+
     PBKDF2 may also be used as a strong salted password hash.  The
     PBKDF2 may also be used as a strong salted password hash.  The
     'crypt' function is provided for that purpose.
     'crypt' function is provided for that purpose.
-    
+
     Remember: Keys generated using PBKDF2 are only as strong as the
     Remember: Keys generated using PBKDF2 are only as strong as the
     passphrases they are derived from.
     passphrases they are derived from.
     """
     """
@@ -122,7 +143,7 @@ class PBKDF2(object):
         """Pseudorandom function.  e.g. HMAC-SHA1"""
         """Pseudorandom function.  e.g. HMAC-SHA1"""
         return self.__macmodule.new(key=key, msg=msg,
         return self.__macmodule.new(key=key, msg=msg,
             digestmod=self.__digestmodule).digest()
             digestmod=self.__digestmodule).digest()
-    
+
     def read(self, bytes):
     def read(self, bytes):
         """Read the specified number of key bytes."""
         """Read the specified number of key bytes."""
         if self.closed:
         if self.closed:
@@ -133,28 +154,28 @@ class PBKDF2(object):
         i = self.__blockNum
         i = self.__blockNum
         while size < bytes:
         while size < bytes:
             i += 1
             i += 1
-            if i > 0xffffffff or i < 1:
-                # We could return "" here, but 
+            if i > _0xffffffffL or i < 1:
+                # We could return "" here, but
                 raise OverflowError("derived key too long")
                 raise OverflowError("derived key too long")
             block = self.__f(i)
             block = self.__f(i)
             blocks.append(block)
             blocks.append(block)
             size += len(block)
             size += len(block)
-        buf = "".join(blocks)
+        buf = b("").join(blocks)
         retval = buf[:bytes]
         retval = buf[:bytes]
         self.__buf = buf[bytes:]
         self.__buf = buf[bytes:]
         self.__blockNum = i
         self.__blockNum = i
         return retval
         return retval
-    
+
     def __f(self, i):
     def __f(self, i):
         # i must fit within 32 bits
         # i must fit within 32 bits
-        assert 1 <= i <= 0xffffffff
+        assert 1 <= i <= _0xffffffffL
         U = self.__prf(self.__passphrase, self.__salt + pack("!L", i))
         U = self.__prf(self.__passphrase, self.__salt + pack("!L", i))
         result = U
         result = U
-        for j in range(2, 1+self.__iterations):
+        for j in xrange(2, 1+self.__iterations):
             U = self.__prf(self.__passphrase, U)
             U = self.__prf(self.__passphrase, U)
-            result = strxor(result, U)
+            result = binxor(result, U)
         return result
         return result
-    
+
     def hexread(self, octets):
     def hexread(self, octets):
         """Read the specified number of octets. Return them as hexadecimal.
         """Read the specified number of octets. Return them as hexadecimal.
 
 
@@ -164,26 +185,26 @@ class PBKDF2(object):
 
 
     def _setup(self, passphrase, salt, iterations, prf):
     def _setup(self, passphrase, salt, iterations, prf):
         # Sanity checks:
         # Sanity checks:
-        
+
         # passphrase and salt must be str or unicode (in the latter
         # passphrase and salt must be str or unicode (in the latter
         # case, we convert to UTF-8)
         # case, we convert to UTF-8)
-        if isinstance(passphrase, str):
+        if isunicode(passphrase):
             passphrase = passphrase.encode("UTF-8")
             passphrase = passphrase.encode("UTF-8")
-        if not isinstance(passphrase, str):
+        elif not isbytes(passphrase):
             raise TypeError("passphrase must be str or unicode")
             raise TypeError("passphrase must be str or unicode")
-        if isinstance(salt, str):
+        if isunicode(salt):
             salt = salt.encode("UTF-8")
             salt = salt.encode("UTF-8")
-        if not isinstance(salt, str):
+        elif not isbytes(salt):
             raise TypeError("salt must be str or unicode")
             raise TypeError("salt must be str or unicode")
 
 
         # iterations must be an integer >= 1
         # iterations must be an integer >= 1
-        if not isinstance(iterations, int):
+        if not isinteger(iterations):
             raise TypeError("iterations must be an integer")
             raise TypeError("iterations must be an integer")
         if iterations < 1:
         if iterations < 1:
             raise ValueError("iterations must be at least 1")
             raise ValueError("iterations must be at least 1")
-        
+
         # prf must be callable
         # prf must be callable
-        if not isinstance(prf, collections.Callable):
+        if not callable(prf):
             raise TypeError("prf must be callable")
             raise TypeError("prf must be callable")
 
 
         self.__passphrase = passphrase
         self.__passphrase = passphrase
@@ -191,9 +212,9 @@ class PBKDF2(object):
         self.__iterations = iterations
         self.__iterations = iterations
         self.__prf = prf
         self.__prf = prf
         self.__blockNum = 0
         self.__blockNum = 0
-        self.__buf = ""
+        self.__buf = b("")
         self.closed = False
         self.closed = False
-    
+
     def close(self):
     def close(self):
         """Close the stream."""
         """Close the stream."""
         if not self.closed:
         if not self.closed:
@@ -207,27 +228,29 @@ class PBKDF2(object):
 
 
 def crypt(word, salt=None, iterations=None):
 def crypt(word, salt=None, iterations=None):
     """PBKDF2-based unix crypt(3) replacement.
     """PBKDF2-based unix crypt(3) replacement.
-    
+
     The number of iterations specified in the salt overrides the 'iterations'
     The number of iterations specified in the salt overrides the 'iterations'
     parameter.
     parameter.
 
 
     The effective hash length is 192 bits.
     The effective hash length is 192 bits.
     """
     """
-    
+
     # Generate a (pseudo-)random salt if the user hasn't provided one.
     # Generate a (pseudo-)random salt if the user hasn't provided one.
     if salt is None:
     if salt is None:
         salt = _makesalt()
         salt = _makesalt()
 
 
     # salt must be a string or the us-ascii subset of unicode
     # salt must be a string or the us-ascii subset of unicode
-    if isinstance(salt, str):
-        salt = salt.encode("us-ascii")
-    if not isinstance(salt, str):
+    if isunicode(salt):
+        salt = salt.encode('us-ascii').decode('us-ascii')
+    elif isbytes(salt):
+        salt = salt.decode('us-ascii')
+    else:
         raise TypeError("salt must be a string")
         raise TypeError("salt must be a string")
 
 
     # word must be a string or unicode (in the latter case, we convert to UTF-8)
     # word must be a string or unicode (in the latter case, we convert to UTF-8)
-    if isinstance(word, str):
+    if isunicode(word):
         word = word.encode("UTF-8")
         word = word.encode("UTF-8")
-    if not isinstance(word, str):
+    elif not isbytes(word):
         raise TypeError("word must be a string or unicode")
         raise TypeError("word must be a string or unicode")
 
 
     # Try to extract the real salt and iteration count from the salt
     # Try to extract the real salt and iteration count from the salt
@@ -242,7 +265,7 @@ def crypt(word, salt=None, iterations=None):
             iterations = converted
             iterations = converted
             if not (iterations >= 1):
             if not (iterations >= 1):
                 raise ValueError("Invalid salt")
                 raise ValueError("Invalid salt")
-    
+
     # Make sure the salt matches the allowed character set
     # Make sure the salt matches the allowed character set
     allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
     allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
     for ch in salt:
     for ch in salt:
@@ -264,92 +287,126 @@ PBKDF2.crypt = staticmethod(crypt)
 
 
 def _makesalt():
 def _makesalt():
     """Return a 48-bit pseudorandom salt for crypt().
     """Return a 48-bit pseudorandom salt for crypt().
-    
+
     This function is not suitable for generating cryptographic secrets.
     This function is not suitable for generating cryptographic secrets.
     """
     """
-    binarysalt = "".join([pack("@H", randint(0, 0xffff)) for i in range(3)])
+    binarysalt = b("").join([pack("@H", randint(0, 0xffff)) for i in range(3)])
     return b64encode(binarysalt, "./")
     return b64encode(binarysalt, "./")
-
-def test_pbkdf2():
-    """Module self-test"""
-    from binascii import a2b_hex
-    
-    #
-    # Test vectors from RFC 3962
-    #
-
-    # Test 1
-    result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1).read(16)
-    expected = a2b_hex("cdedb5281bb2f801565a1122b2563515")
-    if result != expected:
-        raise RuntimeError("self-test failed")
-
-    # Test 2
-    result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1200).hexread(32)
-    expected = ("5c08eb61fdf71e4e4ec3cf6ba1f5512b"
-                "a7e52ddbc5e5142f708a31e2e62b1e13")
-    if result != expected:
-        raise RuntimeError("self-test failed")
-
-    # Test 3
-    result = PBKDF2("X"*64, "pass phrase equals block size", 1200).hexread(32)
-    expected = ("139c30c0966bc32ba55fdbf212530ac9"
-                "c5ec59f1a452f5cc9ad940fea0598ed1")
-    if result != expected:
-        raise RuntimeError("self-test failed")
-    
-    # Test 4
-    result = PBKDF2("X"*65, "pass phrase exceeds block size", 1200).hexread(32)
-    expected = ("9ccad6d468770cd51b10e6a68721be61"
-                "1a8b4d282601db3b36be9246915ec82a")
-    if result != expected:
-        raise RuntimeError("self-test failed")
     
     
-    #
-    # Other test vectors
-    #
-    
-    # Chunked read
-    f = PBKDF2("kickstart", "workbench", 256)
-    result = f.read(17)
-    result += f.read(17)
-    result += f.read(1)
-    result += f.read(2)
-    result += f.read(3)
-    expected = PBKDF2("kickstart", "workbench", 256).read(40)
-    if result != expected:
-        raise RuntimeError("self-test failed")
-    
-    #
-    # crypt() test vectors
-    #
-
-    # crypt 1
-    result = crypt("cloadm", "exec")
-    expected = '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'
-    if result != expected:
-        raise RuntimeError("self-test failed")
-    
-    # crypt 2
-    result = crypt("gnu", '$p5k2$c$u9HvcT4d$.....')
-    expected = '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'
-    if result != expected:
-        raise RuntimeError("self-test failed")
-
-    # crypt 3
-    result = crypt("dcl", "tUsch7fU", iterations=13)
-    expected = "$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL"
-    if result != expected:
-        raise RuntimeError("self-test failed")
-    
-    # crypt 4 (unicode)
-    result = crypt('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2',
-        '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ')
-    expected = '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'
-    if result != expected:
-        raise RuntimeError("self-test failed")
+
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
-    test_pbkdf2()
+    import unittest
+
+
+    class TestPBKDF2(unittest.TestCase):
+        def test_pbkdf2(self):
+            """Module self-test"""
+            from binascii import a2b_hex as _a2b_hex
+            def a2b_hex(s):
+                return _a2b_hex(b(s))
+
+            #
+            # Test vectors from RFC 3962
+            #
+
+            # Test 1
+            result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1).read(16)
+            expected = a2b_hex("cdedb5281bb2f801565a1122b2563515")
+            self.assertEqual(expected, result)
+
+            # Test 2
+            result = PBKDF2("password", "ATHENA.MIT.EDUraeburn", 1200).hexread(32)
+            expected = ("5c08eb61fdf71e4e4ec3cf6ba1f5512b"
+                        "a7e52ddbc5e5142f708a31e2e62b1e13")
+            self.assertEqual(expected, result)
+
+            # Test 3
+            result = PBKDF2("X"*64, "pass phrase equals block size", 1200).hexread(32)
+            expected = ("139c30c0966bc32ba55fdbf212530ac9"
+                        "c5ec59f1a452f5cc9ad940fea0598ed1")
+            self.assertEqual(expected, result)
+
+            # Test 4
+            result = PBKDF2("X"*65, "pass phrase exceeds block size", 1200).hexread(32)
+            expected = ("9ccad6d468770cd51b10e6a68721be61"
+                        "1a8b4d282601db3b36be9246915ec82a")
+            self.assertEqual(expected, result)
+
+            #
+            # Other test vectors
+            #
+
+            # Chunked read
+            f = PBKDF2("kickstart", "workbench", 256)
+            result = f.read(17)
+            result += f.read(17)
+            result += f.read(1)
+            result += f.read(2)
+            result += f.read(3)
+            expected = PBKDF2("kickstart", "workbench", 256).read(40)
+            self.assertEqual(expected, result)
+
+            #
+            # crypt() test vectors
+            #
+
+            # crypt 1
+            result = crypt("cloadm", "exec")
+            expected = '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'
+            self.assertEqual(expected, result)
+
+            # crypt 2
+            result = crypt("gnu", '$p5k2$c$u9HvcT4d$.....')
+            expected = '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'
+            self.assertEqual(expected, result)
+
+            # crypt 3
+            result = crypt("dcl", "tUsch7fU", iterations=13)
+            expected = "$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL"
+            self.assertEqual(expected, result)
+
+            # crypt 4 (unicode)
+            result = crypt(b('\xce\x99\xcf\x89\xce\xb1\xce\xbd\xce\xbd\xce\xb7\xcf\x82').decode('utf-8'),
+                '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ')
+            expected = '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'
+            self.assertEqual(expected, result)
+
+            # crypt 5 (UTF-8 bytes)
+            result = crypt(b('\xce\x99\xcf\x89\xce\xb1\xce\xbd\xce\xbd\xce\xb7\xcf\x82'),
+                '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ')
+            expected = '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'
+            self.assertEqual(expected, result)
+
+        def test_crypt(self):
+            result = crypt("secret")
+            self.assertEqual(result[:6], "$p5k2$")
+
+            result = crypt("secret", "XXXXXXXX")
+            expected = '$p5k2$$XXXXXXXX$L9mVVdq7upotdvtGvXTDTez3FIu3z0uG'
+            self.assertEqual(expected, result)
+
+            # 400 iterations (the default for crypt)
+            result = crypt("secret", "XXXXXXXX", 400)
+            expected = '$p5k2$$XXXXXXXX$L9mVVdq7upotdvtGvXTDTez3FIu3z0uG'
+            self.assertEqual(expected, result)
+
+            # 400 iterations (keyword argument)
+            result = crypt("spam", "FRsH3HJB", iterations=400)
+            expected = '$p5k2$$FRsH3HJB$SgRWDNmB2LukCy0OTal6LYLHZVgtOi7s'
+            self.assertEqual(expected, result)
+
+            # 1000 iterations
+            result = crypt("spam", "H0NX9mT/", iterations=1000)
+            expected = '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'
+            self.assertEqual(expected, result)
+
+            # 1000 iterations (iterations count taken from salt parameter)
+            expected = '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'
+            result = crypt("spam", expected)
+            self.assertEqual(expected, result)
+
+
+    unittest.main(verbosity=2)
 
 
 # vim:set ts=4 sw=4 sts=4 expandtab:
 # vim:set ts=4 sw=4 sts=4 expandtab: