Kaydet (Commit) eab3ff72 authored tarafından Serhiy Storchaka's avatar Serhiy Storchaka Kaydeden (comit) GitHub

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

üst 831d61d5
...@@ -41,17 +41,24 @@ are available on all platforms): ...@@ -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: ...@@ -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
-------- --------
......
...@@ -229,6 +229,12 @@ contextlib ...@@ -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
--- ---
......
...@@ -19,7 +19,7 @@ class _Method(_namedtuple('_Method', 'name ident salt_chars total_size')): ...@@ -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): ...@@ -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): ...@@ -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
import sys
from test import support from test import support
import unittest import unittest
...@@ -6,28 +7,58 @@ crypt = support.import_module('crypt') ...@@ -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()
Added support for the Blowfish hashing in the crypt module.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment