summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/crypt.rst29
-rw-r--r--Doc/whatsnew/3.7.rst6
-rw-r--r--Lib/crypt.py46
-rw-r--r--Lib/test/test_crypt.py53
-rw-r--r--Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst1
5 files changed, 104 insertions, 31 deletions
diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst
index dbd4274..9877b71 100644
--- a/Doc/library/crypt.rst
+++ b/Doc/library/crypt.rst
@@ -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
--------
diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst
index 80f73b6..46121dc 100644
--- a/Doc/whatsnew/3.7.rst
+++ b/Doc/whatsnew/3.7.rst
@@ -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
---
diff --git a/Lib/crypt.py b/Lib/crypt.py
index fbc5f4c..4d73202 100644
--- a/Lib/crypt.py
+++ b/Lib/crypt.py
@@ -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
diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py
index e4f5897..8db1aef 100644
--- a/Lib/test/test_crypt.py
+++ b/Lib/test/test_crypt.py
@@ -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
index 0000000..bd84749
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst
@@ -0,0 +1 @@
+Added support for the Blowfish hashing in the crypt module.