]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-31664: Add support for the Blowfish method in crypt. (#3854)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 24 Oct 2017 16:36:17 +0000 (19:36 +0300)
committerGitHub <noreply@github.com>
Tue, 24 Oct 2017 16:36:17 +0000 (19:36 +0300)
Doc/library/crypt.rst
Doc/whatsnew/3.7.rst
Lib/crypt.py
Lib/test/test_crypt.py
Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst [new file with mode: 0644]

index dbd4274038472ec8ff3a92fa8b248c0f1d8ac4f2..9877b711a9af163dff42a2270d00b89ff2d367c7 100644 (file)
@@ -41,17 +41,24 @@ are available on all platforms):
 .. data:: METHOD_SHA512
 
    A Modular Crypt Format method with 16 character salt and 86 character
-   hash.  This is the strongest method.
+   hash based on the SHA-512 hash function.  This is the strongest method.
 
 .. data:: METHOD_SHA256
 
    Another Modular Crypt Format method with 16 character salt and 43
-   character hash.
+   character hash based on the SHA-256 hash function.
+
+.. data:: METHOD_BLOWFISH
+
+   Another Modular Crypt Format method with 22 character salt and 31
+   character hash based on the Blowfish cipher.
+
+   .. versionadded:: 3.7
 
 .. data:: METHOD_MD5
 
    Another Modular Crypt Format method with 8 character salt and 22
-   character hash.
+   character hash based on the MD5 hash function.
 
 .. data:: METHOD_CRYPT
 
@@ -109,19 +116,25 @@ The :mod:`crypt` module defines the following functions:
       Accept ``crypt.METHOD_*`` values in addition to strings for *salt*.
 
 
-.. function:: mksalt(method=None)
+.. function:: mksalt(method=None, *, log_rounds=12)
 
    Return a randomly generated salt of the specified method.  If no
    *method* is given, the strongest method available as returned by
    :func:`methods` is used.
 
-   The return value is a string either of 2 characters in length for
-   ``crypt.METHOD_CRYPT``, or 19 characters starting with ``$digit$`` and
-   16 random characters from the set ``[./a-zA-Z0-9]``, suitable for
-   passing as the *salt* argument to :func:`crypt`.
+   The return value is a string suitable for passing as the *salt* argument
+   to :func:`crypt`.
+
+   *log_rounds* specifies the binary logarithm of the number of rounds
+   for ``crypt.METHOD_BLOWFISH``, and is ignored otherwise.  ``8`` specifies
+   ``256`` rounds.
 
    .. versionadded:: 3.3
 
+   .. versionchanged:: 3.7
+      Added the *log_rounds* parameter.
+
+
 Examples
 --------
 
index 80f73b6f37ff25bb6df0f7196de92dfab287b49f..46121dcf300d64b43a173c5c2d5cd8c68910d793 100644 (file)
@@ -229,6 +229,12 @@ contextlib
 :func:`contextlib.asynccontextmanager` has been added. (Contributed by
 Jelle Zijlstra in :issue:`29679`.)
 
+crypt
+-----
+
+Added support for the Blowfish method.
+(Contributed by Serhiy Storchaka in :issue:`31664`.)
+
 dis
 ---
 
index fbc5f4cc355ce6fa2bc2b959a2b5b138811d9888..4d73202b468796ca791d4937e1e7adb5ce4ec960 100644 (file)
@@ -19,7 +19,7 @@ class _Method(_namedtuple('_Method', 'name ident salt_chars total_size')):
         return '<crypt.METHOD_{}>'.format(self.name)
 
 
-def mksalt(method=None):
+def mksalt(method=None, *, log_rounds=12):
     """Generate a salt for the specified method.
 
     If not specified, the strongest available method will be used.
@@ -27,7 +27,12 @@ def mksalt(method=None):
     """
     if method is None:
         method = methods[0]
-    s = '${}$'.format(method.ident) if method.ident else ''
+    if not method.ident:
+        s = ''
+    elif method.ident[0] == '2':
+        s = f'${method.ident}${log_rounds:02d}$'
+    else:
+        s = f'${method.ident}$'
     s += ''.join(_sr.choice(_saltchars) for char in range(method.salt_chars))
     return s
 
@@ -48,14 +53,31 @@ def crypt(word, salt=None):
 
 
 #  available salting/crypto methods
-METHOD_CRYPT = _Method('CRYPT', None, 2, 13)
-METHOD_MD5 = _Method('MD5', '1', 8, 34)
-METHOD_SHA256 = _Method('SHA256', '5', 16, 63)
-METHOD_SHA512 = _Method('SHA512', '6', 16, 106)
-
 methods = []
-for _method in (METHOD_SHA512, METHOD_SHA256, METHOD_MD5, METHOD_CRYPT):
-    _result = crypt('', _method)
-    if _result and len(_result) == _method.total_size:
-        methods.append(_method)
-del _result, _method
+
+def _add_method(name, *args):
+    method = _Method(name, *args)
+    globals()['METHOD_' + name] = method
+    salt = mksalt(method, log_rounds=4)
+    result = crypt('', salt)
+    if result and len(result) == method.total_size:
+        methods.append(method)
+        return True
+    return False
+
+_add_method('SHA512', '6', 16, 106)
+_add_method('SHA256', '5', 16, 63)
+
+# Choose the strongest supported version of Blowfish hashing.
+# Early versions have flaws.  Version 'a' fixes flaws of
+# the initial implementation, 'b' fixes flaws of 'a'.
+# 'y' is the same as 'b', for compatibility
+# with openwall crypt_blowfish.
+for _v in 'b', 'y', 'a', '':
+    if _add_method('BLOWFISH', '2' + _v, 22, 59 + len(_v)):
+        break
+
+_add_method('MD5', '1', 8, 34)
+_add_method('CRYPT', None, 2, 13)
+
+del _v, _add_method
index e4f58979c13567276a4f7da23237ab1b8e5e42ce..8db1aefdf1ef2713643708033e913dd30e59b74c 100644 (file)
@@ -1,3 +1,4 @@
+import sys
 from test import support
 import unittest
 
@@ -6,28 +7,58 @@ crypt = support.import_module('crypt')
 class CryptTestCase(unittest.TestCase):
 
     def test_crypt(self):
-        c = crypt.crypt('mypassword', 'ab')
-        if support.verbose:
-            print('Test encryption: ', c)
+        cr = crypt.crypt('mypassword')
+        cr2 = crypt.crypt('mypassword', cr)
+        self.assertEqual(cr2, cr)
+        cr = crypt.crypt('mypassword', 'ab')
+        if cr is not None:
+            cr2 = crypt.crypt('mypassword', cr)
+            self.assertEqual(cr2, cr)
 
     def test_salt(self):
         self.assertEqual(len(crypt._saltchars), 64)
         for method in crypt.methods:
             salt = crypt.mksalt(method)
-            self.assertEqual(len(salt),
-                    method.salt_chars + (3 if method.ident else 0))
+            self.assertIn(len(salt) - method.salt_chars, {0, 1, 3, 4, 6, 7})
+            if method.ident:
+                self.assertIn(method.ident, salt[:len(salt)-method.salt_chars])
 
     def test_saltedcrypt(self):
         for method in crypt.methods:
-            pw = crypt.crypt('assword', method)
-            self.assertEqual(len(pw), method.total_size)
-            pw = crypt.crypt('assword', crypt.mksalt(method))
-            self.assertEqual(len(pw), method.total_size)
+            cr = crypt.crypt('assword', method)
+            self.assertEqual(len(cr), method.total_size)
+            cr2 = crypt.crypt('assword', cr)
+            self.assertEqual(cr2, cr)
+            cr = crypt.crypt('assword', crypt.mksalt(method))
+            self.assertEqual(len(cr), method.total_size)
 
     def test_methods(self):
-        # Guarantee that METHOD_CRYPT is the last method in crypt.methods.
         self.assertTrue(len(crypt.methods) >= 1)
-        self.assertEqual(crypt.METHOD_CRYPT, crypt.methods[-1])
+        if sys.platform.startswith('openbsd'):
+            self.assertEqual(crypt.methods, [crypt.METHOD_BLOWFISH])
+        else:
+            self.assertEqual(crypt.methods[-1], crypt.METHOD_CRYPT)
+
+    @unittest.skipUnless(crypt.METHOD_BLOWFISH in crypt.methods,
+                        'requires support of Blowfish')
+    def test_log_rounds(self):
+        self.assertEqual(len(crypt._saltchars), 64)
+        for log_rounds in range(4, 11):
+            salt = crypt.mksalt(crypt.METHOD_BLOWFISH, log_rounds=log_rounds)
+            self.assertIn('$%02d$' % log_rounds, salt)
+            self.assertIn(len(salt) - crypt.METHOD_BLOWFISH.salt_chars, {6, 7})
+            cr = crypt.crypt('mypassword', salt)
+            self.assertTrue(cr)
+            cr2 = crypt.crypt('mypassword', cr)
+            self.assertEqual(cr2, cr)
+
+    @unittest.skipUnless(crypt.METHOD_BLOWFISH in crypt.methods,
+                        'requires support of Blowfish')
+    def test_invalid_log_rounds(self):
+        for log_rounds in (1, -1, 999):
+            salt = crypt.mksalt(crypt.METHOD_BLOWFISH, log_rounds=log_rounds)
+            self.assertIsNone(crypt.crypt('mypassword', salt))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst b/Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst
new file mode 100644 (file)
index 0000000..bd84749
--- /dev/null
@@ -0,0 +1 @@
+Added support for the Blowfish hashing in the crypt module.