Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions Doc/library/crypt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it still the strongest now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what is stronger, SHA or Blowfish. But since SHA methods were added in glibc when the Blowfish method already was provided by the third-party library, I suppose that at least SHA-512 is not weaker (and likely is stronger) than Blowfish.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Reference for the SHA2 methods is at https://www.akkadia.org/drepper/SHA-crypt.txt


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

Expand Down Expand Up @@ -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
--------

Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down
46 changes: 34 additions & 12 deletions Lib/crypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ def __repr__(self):
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.

"""
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}$'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if log_rounds is more than 99?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log_rounds should be between 4 and 31. crypt() will return None if it is invalid or out of range. Should we silently bound log_rounds into the range 4 to 31, or raise an error, or keep all as is and let crypt() detect an error?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to have a thin wrapper to the C crypt() function. Maybe add unit tests for invalid round values? Negative, 0, 999, etc.?

else:
s = f'${method.ident}$'
s += ''.join(_sr.choice(_saltchars) for char in range(method.salt_chars))
return s

Expand All @@ -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
53 changes: 42 additions & 11 deletions Lib/test/test_crypt.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from test import support
import unittest

Expand All @@ -6,28 +7,58 @@
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for the Blowfish hashing in the crypt module.