bpo-31664: Add support for the Blowfish method in crypt. (#3854)
This commit is contained in:
parent
831d61d56c
commit
eab3ff72eb
|
@ -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
|
||||
--------
|
||||
|
||||
|
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
46
Lib/crypt.py
46
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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Added support for the Blowfish hashing in the crypt module.
|
Loading…
Reference in New Issue