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
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
--------

View 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
---

View 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

View 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()

View File

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