From d1de50dbea1f7b85460ebe512f5d68941cfb662a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 2 Oct 2017 12:49:24 +0300 Subject: [PATCH 1/6] bpo-31664: Add support of Blowfish, extended DES and NT-Hash in crypt. --- Doc/library/crypt.rst | 36 ++++++++++++++++++++++++++----- Doc/whatsnew/3.7.rst | 6 ++++++ Lib/crypt.py | 49 ++++++++++++++++++++++++++++++++---------- Lib/test/test_crypt.py | 43 ++++++++++++++++++++++++++---------- 4 files changed, 107 insertions(+), 27 deletions(-) diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst index dbd4274038472e..10dcfad46929e8 100644 --- a/Doc/library/crypt.rst +++ b/Doc/library/crypt.rst @@ -48,11 +48,32 @@ are available on all platforms): Another Modular Crypt Format method with 16 character salt and 43 character hash. +.. data:: METHOD_BLF + + Another Modular Crypt Format method with 22 character salt and 31 + character hash. + + .. versionadded:: 3.7 + .. data:: METHOD_MD5 Another Modular Crypt Format method with 8 character salt and 22 character hash. +.. data:: METHOD_DES + + Another Modular Crypt Format method with 8 character salt and 11 + character hash. + + .. versionadded:: 3.7 + +.. data:: METHOD_NTH + + Another Modular Crypt Format method without a salt and 32 + hexadecimal characters of hash. + + .. versionadded:: 3.7 + .. data:: METHOD_CRYPT The traditional method with a 2 character salt and 13 characters of @@ -109,19 +130,24 @@ 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_round* specifies the binary logarithm of the number of rounds + for ``crypt.METHOD_CRYPT``. ``8`` specifies ``256`` rounds. .. versionadded:: 3.3 + .. versionchanged:: 3.7 + Added the *log_round* parameter. + + Examples -------- diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 845ed643f97b5b..8489c143ec9768 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -178,6 +178,12 @@ contextlib :func:`contextlib.asynccontextmanager` has been added. (Contributed by Jelle Zijlstra in :issue:`29679`.) +crypt +----- + +Added support for Blowfish, extended DES and NT-Hash methods. +(Contributed by Serhiy Storchaka in :issue:`31664`.) + dis --- diff --git a/Lib/crypt.py b/Lib/crypt.py index fbc5f4cc355ce6..de605ad499a89c 100644 --- a/Lib/crypt.py +++ b/Lib/crypt.py @@ -19,7 +19,7 @@ def __repr__(self): return ''.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,16 @@ 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 == '_': + s = method.ident + elif method.ident[0] == '2': + s = f'${method.ident}${log_rounds:02d}$' + elif method.ident == '3': + return f'${method.ident}$$' + else: + s = f'${method.ident}$' s += ''.join(_sr.choice(_saltchars) for char in range(method.salt_chars)) return s @@ -48,14 +57,32 @@ 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) +methods = [] + +def _probe(method): + result = crypt('', method) + if result and len(result) == method.total_size: + methods.append(method) + return True + return False + METHOD_SHA512 = _Method('SHA512', '6', 16, 106) +_probe(METHOD_SHA512) +METHOD_SHA256 = _Method('SHA256', '5', 16, 63) +_probe(METHOD_SHA256) -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 +for _v in 'b', 'y', 'a', '': + METHOD_BLF = _Method('BLF', '2' + _v, 22, 59 + len(_v)) + if _probe(METHOD_BLF): + break + +METHOD_MD5 = _Method('MD5', '1', 8, 34) +_probe(METHOD_MD5) +METHOD_DES = _Method('DES', '_', 8, 20) +_probe(METHOD_DES) +METHOD_NTH = _Method('NTH', '3', 0, 36) +_probe(METHOD_NTH) +METHOD_CRYPT = _Method('CRYPT', None, 2, 13) +_probe(METHOD_CRYPT) + +del _v, _probe diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py index e4f58979c13567..76888310fb230d 100644 --- a/Lib/test/test_crypt.py +++ b/Lib/test/test_crypt.py @@ -1,3 +1,4 @@ +import sys from test import support import unittest @@ -6,28 +7,48 @@ 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_BLF]) + else: + self.assertEqual(crypt.methods[-1], crypt.METHOD_CRYPT) + + @unittest.skipUnless(crypt.METHOD_BLF in crypt.methods, + 'requires support of Blowfish') + def test_log_rounds(self): + self.assertEqual(len(crypt._saltchars), 64) + for log_rounds in range(4, 13): + salt = crypt.mksalt(crypt.METHOD_BLF, log_rounds=log_rounds) + self.assertTrue(salt) + self.assertIn('$%02d$' % log_rounds, salt) + self.assertIn(len(salt) - crypt.METHOD_BLF.salt_chars, {6, 7}) + if __name__ == "__main__": unittest.main() From b75737a7cf5c6a544e4ac3eacf99d92dc8996dc0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 2 Oct 2017 20:48:31 +0300 Subject: [PATCH 2/6] Remove DES and NT-Hash methods, fix some errors and improve documentation. --- Doc/library/crypt.rst | 24 +++++------------------- Doc/whatsnew/3.7.rst | 2 +- Lib/crypt.py | 32 +++++++++++--------------------- 3 files changed, 17 insertions(+), 41 deletions(-) diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst index 10dcfad46929e8..0c08d83f79009e 100644 --- a/Doc/library/crypt.rst +++ b/Doc/library/crypt.rst @@ -41,38 +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 SSH-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 SSH-256 hash function. .. data:: METHOD_BLF Another Modular Crypt Format method with 22 character salt and 31 - character hash. + 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. - -.. data:: METHOD_DES - - Another Modular Crypt Format method with 8 character salt and 11 - character hash. - - .. versionadded:: 3.7 - -.. data:: METHOD_NTH - - Another Modular Crypt Format method without a salt and 32 - hexadecimal characters of hash. - - .. versionadded:: 3.7 + character hash based on the MD5 hash function. .. data:: METHOD_CRYPT @@ -140,7 +126,7 @@ The :mod:`crypt` module defines the following functions: to :func:`crypt`. *log_round* specifies the binary logarithm of the number of rounds - for ``crypt.METHOD_CRYPT``. ``8`` specifies ``256`` rounds. + for ``crypt.METHOD_BLF``. ``8`` specifies ``256`` rounds. .. versionadded:: 3.3 diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 8489c143ec9768..9a8663b9f6ccaf 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -181,7 +181,7 @@ Jelle Zijlstra in :issue:`29679`.) crypt ----- -Added support for Blowfish, extended DES and NT-Hash methods. +Added support for the Blowfish method. (Contributed by Serhiy Storchaka in :issue:`31664`.) dis diff --git a/Lib/crypt.py b/Lib/crypt.py index de605ad499a89c..0cb3a170eaaa74 100644 --- a/Lib/crypt.py +++ b/Lib/crypt.py @@ -29,12 +29,8 @@ def mksalt(method=None, *, log_rounds=12): method = methods[0] if not method.ident: s = '' - elif method.ident == '_': - s = method.ident elif method.ident[0] == '2': s = f'${method.ident}${log_rounds:02d}$' - elif method.ident == '3': - return f'${method.ident}$$' else: s = f'${method.ident}$' s += ''.join(_sr.choice(_saltchars) for char in range(method.salt_chars)) @@ -59,30 +55,24 @@ def crypt(word, salt=None): # available salting/crypto methods methods = [] -def _probe(method): - result = crypt('', 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 -METHOD_SHA512 = _Method('SHA512', '6', 16, 106) -_probe(METHOD_SHA512) -METHOD_SHA256 = _Method('SHA256', '5', 16, 63) -_probe(METHOD_SHA256) +_add_method('SHA512', '6', 16, 106) +_add_method('SHA256', '5', 16, 63) for _v in 'b', 'y', 'a', '': - METHOD_BLF = _Method('BLF', '2' + _v, 22, 59 + len(_v)) - if _probe(METHOD_BLF): + if _add_method('BLF', '2' + _v, 22, 59 + len(_v)): break -METHOD_MD5 = _Method('MD5', '1', 8, 34) -_probe(METHOD_MD5) -METHOD_DES = _Method('DES', '_', 8, 20) -_probe(METHOD_DES) -METHOD_NTH = _Method('NTH', '3', 0, 36) -_probe(METHOD_NTH) -METHOD_CRYPT = _Method('CRYPT', None, 2, 13) -_probe(METHOD_CRYPT) +_add_method('MD5', '1', 8, 34) +_add_method('CRYPT', None, 2, 13) -del _v, _probe +del _v, _add_method From 772848cdf52508db6bc4510838f376efef78def1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 4 Oct 2017 20:36:40 +0300 Subject: [PATCH 3/6] Add a NEWS.d entry. --- .../NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst diff --git a/Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst b/Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst new file mode 100644 index 00000000000000..bd8474977d87d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-10-04-20-36-28.bpo-31664.4VDUzo.rst @@ -0,0 +1 @@ +Added support for the Blowfish hashing in the crypt module. From 9dbf23a7b513d57e29b5351f83c1f96bd3600e96 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 5 Oct 2017 10:25:59 +0300 Subject: [PATCH 4/6] Update docs. --- Doc/library/crypt.rst | 11 ++++++----- Lib/crypt.py | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst index 0c08d83f79009e..ddf2f92b562ab5 100644 --- a/Doc/library/crypt.rst +++ b/Doc/library/crypt.rst @@ -41,12 +41,12 @@ are available on all platforms): .. data:: METHOD_SHA512 A Modular Crypt Format method with 16 character salt and 86 character - hash based on the SSH-512 hash function. 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 based on the SSH-256 hash function. + character hash based on the SHA-256 hash function. .. data:: METHOD_BLF @@ -125,13 +125,14 @@ The :mod:`crypt` module defines the following functions: The return value is a string suitable for passing as the *salt* argument to :func:`crypt`. - *log_round* specifies the binary logarithm of the number of rounds - for ``crypt.METHOD_BLF``. ``8`` specifies ``256`` rounds. + *log_rounds* specifies the binary logarithm of the number of rounds + for ``crypt.METHOD_BLF``, and is ignored otherwise. ``8`` specifies + ``256`` rounds. .. versionadded:: 3.3 .. versionchanged:: 3.7 - Added the *log_round* parameter. + Added the *log_rounds* parameter. Examples diff --git a/Lib/crypt.py b/Lib/crypt.py index 0cb3a170eaaa74..f65fa0a36c04f3 100644 --- a/Lib/crypt.py +++ b/Lib/crypt.py @@ -68,6 +68,11 @@ def _add_method(name, *args): _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('BLF', '2' + _v, 22, 59 + len(_v)): break From ba2f0033e8629a34d980f4344cc6aa7cd3a2303c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 5 Oct 2017 15:45:38 +0300 Subject: [PATCH 5/6] Add tests for invalid log_rounds. --- Lib/test/test_crypt.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py index 76888310fb230d..762079a6abe6b4 100644 --- a/Lib/test/test_crypt.py +++ b/Lib/test/test_crypt.py @@ -43,11 +43,21 @@ def test_methods(self): 'requires support of Blowfish') def test_log_rounds(self): self.assertEqual(len(crypt._saltchars), 64) - for log_rounds in range(4, 13): + for log_rounds in range(4, 11): salt = crypt.mksalt(crypt.METHOD_BLF, log_rounds=log_rounds) - self.assertTrue(salt) self.assertIn('$%02d$' % log_rounds, salt) self.assertIn(len(salt) - crypt.METHOD_BLF.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_BLF 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_BLF, log_rounds=log_rounds) + self.assertIsNone(crypt.crypt('mypassword', salt)) if __name__ == "__main__": From 6aa4d0d8ccc05d75e067702161ae7fea734f3959 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 22 Oct 2017 20:35:22 +0300 Subject: [PATCH 6/6] Rename BLF to BLOWFISH. --- Doc/library/crypt.rst | 4 ++-- Lib/crypt.py | 2 +- Lib/test/test_crypt.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst index ddf2f92b562ab5..9877b711a9af16 100644 --- a/Doc/library/crypt.rst +++ b/Doc/library/crypt.rst @@ -48,7 +48,7 @@ are available on all platforms): Another Modular Crypt Format method with 16 character salt and 43 character hash based on the SHA-256 hash function. -.. data:: METHOD_BLF +.. data:: METHOD_BLOWFISH Another Modular Crypt Format method with 22 character salt and 31 character hash based on the Blowfish cipher. @@ -126,7 +126,7 @@ The :mod:`crypt` module defines the following functions: to :func:`crypt`. *log_rounds* specifies the binary logarithm of the number of rounds - for ``crypt.METHOD_BLF``, and is ignored otherwise. ``8`` specifies + for ``crypt.METHOD_BLOWFISH``, and is ignored otherwise. ``8`` specifies ``256`` rounds. .. versionadded:: 3.3 diff --git a/Lib/crypt.py b/Lib/crypt.py index f65fa0a36c04f3..4d73202b468796 100644 --- a/Lib/crypt.py +++ b/Lib/crypt.py @@ -74,7 +74,7 @@ def _add_method(name, *args): # 'y' is the same as 'b', for compatibility # with openwall crypt_blowfish. for _v in 'b', 'y', 'a', '': - if _add_method('BLF', '2' + _v, 22, 59 + len(_v)): + if _add_method('BLOWFISH', '2' + _v, 22, 59 + len(_v)): break _add_method('MD5', '1', 8, 34) diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py index 762079a6abe6b4..8db1aefdf1ef27 100644 --- a/Lib/test/test_crypt.py +++ b/Lib/test/test_crypt.py @@ -35,28 +35,28 @@ def test_saltedcrypt(self): def test_methods(self): self.assertTrue(len(crypt.methods) >= 1) if sys.platform.startswith('openbsd'): - self.assertEqual(crypt.methods, [crypt.METHOD_BLF]) + self.assertEqual(crypt.methods, [crypt.METHOD_BLOWFISH]) else: self.assertEqual(crypt.methods[-1], crypt.METHOD_CRYPT) - @unittest.skipUnless(crypt.METHOD_BLF in crypt.methods, + @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_BLF, log_rounds=log_rounds) + salt = crypt.mksalt(crypt.METHOD_BLOWFISH, log_rounds=log_rounds) self.assertIn('$%02d$' % log_rounds, salt) - self.assertIn(len(salt) - crypt.METHOD_BLF.salt_chars, {6, 7}) + 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_BLF in crypt.methods, + @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_BLF, log_rounds=log_rounds) + salt = crypt.mksalt(crypt.METHOD_BLOWFISH, log_rounds=log_rounds) self.assertIsNone(crypt.crypt('mypassword', salt))