From ff2c869fc8d91d8bd2eeaedb2e5d2384d0fb536e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Esser?= Date: Fri, 4 Jun 2021 12:37:31 +0200 Subject: [PATCH 1/3] bpo-44309: Add support for yescrypt in crypt. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://bugs.python.org/issue44309 Signed-off-by: Björn Esser --- Doc/library/crypt.rst | 11 +++++++- Lib/crypt.py | 18 ++++++++++++- Lib/test/test_crypt.py | 27 +++++++++++++++++++ .../2021-06-04-11-19-28.bpo-44309.PBcd1f.rst | 1 + 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-06-04-11-19-28.bpo-44309.PBcd1f.rst diff --git a/Doc/library/crypt.rst b/Doc/library/crypt.rst index d25c626a175854a..50d949244f64253 100644 --- a/Doc/library/crypt.rst +++ b/Doc/library/crypt.rst @@ -43,7 +43,8 @@ are available on all platforms): .. data:: METHOD_SHA512 A Modular Crypt Format method with 16 character salt and 86 character - hash based on the SHA-512 hash function. This is the strongest method. + hash based on the SHA-512 hash function. This is the strongest method + supported on all UNIX-based operating systems. .. data:: METHOD_SHA256 @@ -67,6 +68,14 @@ are available on all platforms): The traditional method with a 2 character salt and 13 characters of hash. This is the weakest method. + .. versionadded:: 3.10 + +.. data:: METHOD_YESCRYPT + + Another Modular Crypt Format method with 24 character salt and 43 + character hash based on the yescrypt hash function. This is the + strongest method supported on Linux distributions using libxcrypt. + Module Attributes ----------------- diff --git a/Lib/crypt.py b/Lib/crypt.py index 33dbc46bb3e96be..d06b1147ae66ea7 100644 --- a/Lib/crypt.py +++ b/Lib/crypt.py @@ -45,7 +45,19 @@ def mksalt(method=None, *, rounds=None): else: # modular s = f'${method.ident}$' - if method.ident and method.ident[0] == '2': # Blowfish variants + if method.ident and method.ident == 'y': # yescrypt + if rounds is not None: + if not 1 <= rounds <= 11: + raise ValueError('rounds out of the range 1 to 11') + else: + rounds = 5 + if rounds < 3: + s += 'j' + chr(54 + rounds) + '5$' + elif rounds < 6: + s += 'j' + chr(52 + rounds) + 'T$' + elif rounds < 12: + s += 'j' + chr(59 + rounds) + 'T$' + elif method.ident and method.ident[0] == '2': # Blowfish variants if rounds is None: log_rounds = 12 else: @@ -102,6 +114,10 @@ def _add_method(name, *args, rounds=None): return True return False +# Supported by libxcrypt. Strongest hashing method currently supported. +_add_method('YESCRYPT', 'y', 24, 75, rounds=1) + +# SHA-2 based methods. _add_method('SHA512', '6', 16, 106) _add_method('SHA256', '5', 16, 63) diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py index 5dc83b4ecbfa008..1286c888e354686 100644 --- a/Lib/test/test_crypt.py +++ b/Lib/test/test_crypt.py @@ -88,7 +88,34 @@ def test_blowfish_rounds(self): cr2 = crypt.crypt('mypassword', cr) self.assertEqual(cr2, cr) + @unittest.skipUnless( + crypt and crypt.METHOD_YESCRYPT in crypt.methods, 'requires support of yescrypt' + ) + def test_yescrypt_rounds(self): + for rounds in range(1, 11): + if rounds < 3: + enc_rounds = 'j' + chr(54 + rounds) + '5' + elif rounds < 6: + enc_rounds = 'j' + chr(52 + rounds) + 'T' + elif rounds < 12: + enc_rounds = 'j' + chr(59 + rounds) + 'T' + salt = crypt.mksalt(crypt.METHOD_YESCRYPT, rounds=rounds) + self.assertIn('$%s$' % enc_rounds, salt) + self.assertEqual(len(salt) - crypt.METHOD_YESCRYPT.salt_chars, 7) + cr = crypt.crypt('mypassword', salt) + self.assertTrue(cr) + cr2 = crypt.crypt('mypassword', cr) + self.assertEqual(cr2, cr) + def test_invalid_rounds(self): + if crypt.METHOD_YESCRYPT in crypt.methods: + with self.assertRaises(TypeError): + crypt.mksalt(crypt.METHOD_YESCRYPT, rounds='4096') + with self.assertRaises(TypeError): + crypt.mksalt(crypt.METHOD_YESCRYPT, rounds=4096.0) + for rounds in (0, -1, 1000, 1<<999): + with self.assertRaises(ValueError): + crypt.mksalt(crypt.METHOD_YESCRYPT, rounds=rounds) for method in (crypt.METHOD_SHA256, crypt.METHOD_SHA512, crypt.METHOD_BLOWFISH): with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Library/2021-06-04-11-19-28.bpo-44309.PBcd1f.rst b/Misc/NEWS.d/next/Library/2021-06-04-11-19-28.bpo-44309.PBcd1f.rst new file mode 100644 index 000000000000000..0056ee81e23ea88 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-04-11-19-28.bpo-44309.PBcd1f.rst @@ -0,0 +1 @@ +Add support for the yescrypt hashing method in the crypt module. \ No newline at end of file From 49913b26302c479f128de3dc6a2b61a64927fac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Esser?= Date: Sat, 5 Jun 2021 12:02:32 +0200 Subject: [PATCH 2/3] Changes as suggested by @serhiy-storchaka Co-authored-by: Serhiy Storchaka --- Lib/crypt.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/crypt.py b/Lib/crypt.py index d06b1147ae66ea7..98aa43c5ce32068 100644 --- a/Lib/crypt.py +++ b/Lib/crypt.py @@ -51,12 +51,7 @@ def mksalt(method=None, *, rounds=None): raise ValueError('rounds out of the range 1 to 11') else: rounds = 5 - if rounds < 3: - s += 'j' + chr(54 + rounds) + '5$' - elif rounds < 6: - s += 'j' + chr(52 + rounds) + 'T$' - elif rounds < 12: - s += 'j' + chr(59 + rounds) + 'T$' + s += 'j' + ('75', '85', '7T', '8T', '9T', 'AT', 'BT', 'CT', 'DT', 'ET', 'FT')[rounds - 1] + '$' elif method.ident and method.ident[0] == '2': # Blowfish variants if rounds is None: log_rounds = 12 From 72e000997c1553e7668a815b2034daf112fab75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Esser?= Date: Sat, 5 Jun 2021 12:11:15 +0200 Subject: [PATCH 3/3] Adapt previous changes in test_crypt --- Lib/test/test_crypt.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/test/test_crypt.py b/Lib/test/test_crypt.py index 1286c888e354686..7d22c87496180dd 100644 --- a/Lib/test/test_crypt.py +++ b/Lib/test/test_crypt.py @@ -93,12 +93,7 @@ def test_blowfish_rounds(self): ) def test_yescrypt_rounds(self): for rounds in range(1, 11): - if rounds < 3: - enc_rounds = 'j' + chr(54 + rounds) + '5' - elif rounds < 6: - enc_rounds = 'j' + chr(52 + rounds) + 'T' - elif rounds < 12: - enc_rounds = 'j' + chr(59 + rounds) + 'T' + enc_rounds = 'j' + ('75', '85', '7T', '8T', '9T', 'AT', 'BT', 'CT', 'DT', 'ET', 'FT')[rounds - 1] salt = crypt.mksalt(crypt.METHOD_YESCRYPT, rounds=rounds) self.assertIn('$%s$' % enc_rounds, salt) self.assertEqual(len(salt) - crypt.METHOD_YESCRYPT.salt_chars, 7)