bpo-31664: Add support for the Blowfish method in crypt. (#3854)

This commit is contained in:
Serhiy Storchaka 2017-10-24 19:36:17 +03:00 committed by GitHub
parent 831d61d56c
commit eab3ff72eb
5 changed files with 104 additions and 31 deletions

View File

@ -41,17 +41,24 @@ are available on all platforms):
.. data:: METHOD_SHA512 .. data:: METHOD_SHA512
A Modular Crypt Format method with 16 character salt and 86 character 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 .. data:: METHOD_SHA256
Another Modular Crypt Format method with 16 character salt and 43 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 .. data:: METHOD_MD5
Another Modular Crypt Format method with 8 character salt and 22 Another Modular Crypt Format method with 8 character salt and 22
character hash. character hash based on the MD5 hash function.
.. data:: METHOD_CRYPT .. 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*. 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 Return a randomly generated salt of the specified method. If no
*method* is given, the strongest method available as returned by *method* is given, the strongest method available as returned by
:func:`methods` is used. :func:`methods` is used.
The return value is a string either of 2 characters in length for The return value is a string suitable for passing as the *salt* argument
``crypt.METHOD_CRYPT``, or 19 characters starting with ``$digit$`` and to :func:`crypt`.
16 random characters from the set ``[./a-zA-Z0-9]``, 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 .. versionadded:: 3.3
.. versionchanged:: 3.7
Added the *log_rounds* parameter.
Examples Examples
-------- --------

View File

@ -229,6 +229,12 @@ contextlib
:func:`contextlib.asynccontextmanager` has been added. (Contributed by :func:`contextlib.asynccontextmanager` has been added. (Contributed by
Jelle Zijlstra in :issue:`29679`.) Jelle Zijlstra in :issue:`29679`.)
crypt
-----
Added support for the Blowfish method.
(Contributed by Serhiy Storchaka in :issue:`31664`.)
dis dis
--- ---

View File

@ -19,7 +19,7 @@ class _Method(_namedtuple('_Method', 'name ident salt_chars total_size')):
return '<crypt.METHOD_{}>'.format(self.name) return '<crypt.METHOD_{}>'.format(self.name)
def mksalt(method=None): def mksalt(method=None, *, log_rounds=12):
"""Generate a salt for the specified method. """Generate a salt for the specified method.
If not specified, the strongest available method will be used. If not specified, the strongest available method will be used.
@ -27,7 +27,12 @@ def mksalt(method=None):
""" """
if method is None: if method is None:
method = methods[0] 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)) s += ''.join(_sr.choice(_saltchars) for char in range(method.salt_chars))
return s return s
@ -48,14 +53,31 @@ def crypt(word, salt=None):
# available salting/crypto methods # 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 = [] methods = []
for _method in (METHOD_SHA512, METHOD_SHA256, METHOD_MD5, METHOD_CRYPT):
_result = crypt('', _method) def _add_method(name, *args):
if _result and len(_result) == _method.total_size: method = _Method(name, *args)
methods.append(_method) globals()['METHOD_' + name] = method
del _result, _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

View File

@ -1,3 +1,4 @@
import sys
from test import support from test import support
import unittest import unittest
@ -6,28 +7,58 @@ crypt = support.import_module('crypt')
class CryptTestCase(unittest.TestCase): class CryptTestCase(unittest.TestCase):
def test_crypt(self): def test_crypt(self):
c = crypt.crypt('mypassword', 'ab') cr = crypt.crypt('mypassword')
if support.verbose: cr2 = crypt.crypt('mypassword', cr)
print('Test encryption: ', c) 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): def test_salt(self):
self.assertEqual(len(crypt._saltchars), 64) self.assertEqual(len(crypt._saltchars), 64)
for method in crypt.methods: for method in crypt.methods:
salt = crypt.mksalt(method) salt = crypt.mksalt(method)
self.assertEqual(len(salt), self.assertIn(len(salt) - method.salt_chars, {0, 1, 3, 4, 6, 7})
method.salt_chars + (3 if method.ident else 0)) if method.ident:
self.assertIn(method.ident, salt[:len(salt)-method.salt_chars])
def test_saltedcrypt(self): def test_saltedcrypt(self):
for method in crypt.methods: for method in crypt.methods:
pw = crypt.crypt('assword', method) cr = crypt.crypt('assword', method)
self.assertEqual(len(pw), method.total_size) self.assertEqual(len(cr), method.total_size)
pw = crypt.crypt('assword', crypt.mksalt(method)) cr2 = crypt.crypt('assword', cr)
self.assertEqual(len(pw), method.total_size) self.assertEqual(cr2, cr)
cr = crypt.crypt('assword', crypt.mksalt(method))
self.assertEqual(len(cr), method.total_size)
def test_methods(self): def test_methods(self):
# Guarantee that METHOD_CRYPT is the last method in crypt.methods.
self.assertTrue(len(crypt.methods) >= 1) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -0,0 +1 @@
Added support for the Blowfish hashing in the crypt module.