Skip to content

Commit a68ddea

Browse files
authored
gh-90733: improve hashlib.scrypt interface (#136100)
* add `scrypt` to `hashlib.__all__` * improve `hashlib.scrypt` exception messages
1 parent 75e2c5d commit a68ddea

File tree

5 files changed

+120
-88
lines changed

5 files changed

+120
-88
lines changed

Lib/hashlib.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ def __hash_new(name, *args, **kwargs):
187187

188188
try:
189189
# OpenSSL's scrypt requires OpenSSL 1.1+
190-
from _hashlib import scrypt # noqa: F401
190+
from _hashlib import scrypt
191+
__all__ += ('scrypt',)
191192
except ImportError:
192193
pass
193194

Lib/test/test_hashlib.py

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,12 +1184,6 @@ class KDFTests(unittest.TestCase):
11841184
(b'pass\0word', b'sa\0lt', 4096, 16),
11851185
]
11861186

1187-
scrypt_test_vectors = [
1188-
(b'', b'', 16, 1, 1, unhexlify('77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906')),
1189-
(b'password', b'NaCl', 1024, 8, 16, unhexlify('fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640')),
1190-
(b'pleaseletmein', b'SodiumChloride', 16384, 8, 1, unhexlify('7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887')),
1191-
]
1192-
11931187
pbkdf2_results = {
11941188
"sha1": [
11951189
# official test vectors from RFC 6070
@@ -1281,46 +1275,6 @@ def _test_pbkdf2_hmac(self, pbkdf2, supported):
12811275
def test_pbkdf2_hmac_c(self):
12821276
self._test_pbkdf2_hmac(openssl_hashlib.pbkdf2_hmac, openssl_md_meth_names)
12831277

1284-
@unittest.skipUnless(hasattr(hashlib, 'scrypt'),
1285-
' test requires OpenSSL > 1.1')
1286-
@unittest.skipIf(get_fips_mode(), reason="scrypt is blocked in FIPS mode")
1287-
def test_scrypt(self):
1288-
for password, salt, n, r, p, expected in self.scrypt_test_vectors:
1289-
result = hashlib.scrypt(password, salt=salt, n=n, r=r, p=p)
1290-
self.assertEqual(result, expected)
1291-
1292-
# this values should work
1293-
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1)
1294-
# password and salt must be bytes-like
1295-
with self.assertRaises(TypeError):
1296-
hashlib.scrypt('password', salt=b'salt', n=2, r=8, p=1)
1297-
with self.assertRaises(TypeError):
1298-
hashlib.scrypt(b'password', salt='salt', n=2, r=8, p=1)
1299-
# require keyword args
1300-
with self.assertRaises(TypeError):
1301-
hashlib.scrypt(b'password')
1302-
with self.assertRaises(TypeError):
1303-
hashlib.scrypt(b'password', b'salt')
1304-
with self.assertRaises(TypeError):
1305-
hashlib.scrypt(b'password', 2, 8, 1, salt=b'salt')
1306-
for n in [-1, 0, 1, None]:
1307-
with self.assertRaises((ValueError, OverflowError, TypeError)):
1308-
hashlib.scrypt(b'password', salt=b'salt', n=n, r=8, p=1)
1309-
for r in [-1, 0, None]:
1310-
with self.assertRaises((ValueError, OverflowError, TypeError)):
1311-
hashlib.scrypt(b'password', salt=b'salt', n=2, r=r, p=1)
1312-
for p in [-1, 0, None]:
1313-
with self.assertRaises((ValueError, OverflowError, TypeError)):
1314-
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=p)
1315-
for maxmem in [-1, None]:
1316-
with self.assertRaises((ValueError, OverflowError, TypeError)):
1317-
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1,
1318-
maxmem=maxmem)
1319-
for dklen in [-1, None]:
1320-
with self.assertRaises((ValueError, OverflowError, TypeError)):
1321-
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1,
1322-
dklen=dklen)
1323-
13241278
def test_normalized_name(self):
13251279
self.assertNotIn("blake2b512", hashlib.algorithms_available)
13261280
self.assertNotIn("sha3-512", hashlib.algorithms_available)
@@ -1362,5 +1316,76 @@ def readable(self):
13621316
hashlib.file_digest(NonBlocking(), hashlib.sha256)
13631317

13641318

1319+
@unittest.skipUnless(hasattr(hashlib, 'scrypt'), 'requires OpenSSL 1.1+')
1320+
@unittest.skipIf(get_fips_mode(), reason="scrypt is blocked in FIPS mode")
1321+
class TestScrypt(unittest.TestCase):
1322+
1323+
scrypt_test_vectors = [
1324+
(b'', b'', 16, 1, 1, unhexlify('77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906')),
1325+
(b'password', b'NaCl', 1024, 8, 16, unhexlify('fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640')),
1326+
(b'pleaseletmein', b'SodiumChloride', 16384, 8, 1, unhexlify('7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887')),
1327+
]
1328+
1329+
def test_scrypt(self):
1330+
for password, salt, n, r, p, expected in self.scrypt_test_vectors:
1331+
result = hashlib.scrypt(password, salt=salt, n=n, r=r, p=p)
1332+
self.assertEqual(result, expected)
1333+
1334+
# these parameters must be valid
1335+
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1)
1336+
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1, maxmem=0)
1337+
hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1, dklen=1)
1338+
1339+
def test_scrypt_types(self):
1340+
# password and salt must be bytes-like
1341+
with self.assertRaises(TypeError):
1342+
hashlib.scrypt('password', salt=b'salt', n=2, r=8, p=1)
1343+
with self.assertRaises(TypeError):
1344+
hashlib.scrypt(b'password', salt='salt', n=2, r=8, p=1)
1345+
# require keyword args
1346+
with self.assertRaises(TypeError):
1347+
hashlib.scrypt(b'password')
1348+
with self.assertRaises(TypeError):
1349+
hashlib.scrypt(b'password', b'salt')
1350+
with self.assertRaises(TypeError):
1351+
hashlib.scrypt(b'password', 2, 8, 1, salt=b'salt')
1352+
1353+
def test_scrypt_validate(self):
1354+
def scrypt(password=b"password", /, **kwargs):
1355+
# overwrite well-defined parameters with bad ones
1356+
kwargs = dict(salt=b'salt', n=2, r=8, p=1) | kwargs
1357+
return hashlib.scrypt(password, **kwargs)
1358+
1359+
for param_name in ('n', 'r', 'p', 'maxmem', 'dklen'):
1360+
param = {param_name: None}
1361+
with self.subTest(**param):
1362+
self.assertRaises(TypeError, scrypt, **param)
1363+
1364+
self.assertRaises(ValueError, scrypt, n=0)
1365+
self.assertRaises(ValueError, scrypt, n=-1)
1366+
self.assertRaises(ValueError, scrypt, n=1)
1367+
1368+
self.assertRaises(ValueError, scrypt, r=0)
1369+
self.assertRaises(ValueError, scrypt, r=-1)
1370+
1371+
self.assertRaises(ValueError, scrypt, p=-1)
1372+
self.assertRaises(ValueError, scrypt, p=0)
1373+
1374+
self.assertRaises(ValueError, scrypt, maxmem=-1)
1375+
# OpenSSL hard limit for 'maxmem' is an 'uint64_t' but for now,
1376+
# we do not use the 'uint64' Clinic converter but the 'long' one.
1377+
self.assertRaises(OverflowError, scrypt, maxmem=(1 << 64))
1378+
# Historically, Python allowed 'maxmem' to be at most INT_MAX,
1379+
# which is at most 2**32-1 (on Windows, sizeof(long) == 4, so
1380+
# an OverflowError will be raised instead of a ValueError).
1381+
numeric_exc_types = (OverflowError, ValueError)
1382+
self.assertRaises(numeric_exc_types, scrypt, maxmem=(1 << 32))
1383+
1384+
self.assertRaises(ValueError, scrypt, dklen=-1)
1385+
self.assertRaises(ValueError, scrypt, dklen=0)
1386+
MAX_DKLEN = ((1 << 32) - 1) * 32 # see RFC 7914
1387+
self.assertRaises(numeric_exc_types, scrypt, dklen=MAX_DKLEN + 1)
1388+
1389+
13651390
if __name__ == "__main__":
13661391
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve error messages when reporting invalid parameters in
2+
:func:`hashlib.scrypt`. Patch by Bénédikt Tran.

Modules/_hashopenssl.c

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848

4949
#define MUNCH_SIZE INT_MAX
5050

51-
#define PY_OPENSSL_HAS_SCRYPT 1
5251
#if defined(NID_sha3_224) && defined(NID_sha3_256) && defined(NID_sha3_384) && defined(NID_sha3_512)
5352
#define PY_OPENSSL_HAS_SHA3 1
5453
#endif
@@ -1557,7 +1556,25 @@ pbkdf2_hmac_impl(PyObject *module, const char *hash_name,
15571556
return key_obj;
15581557
}
15591558

1560-
#ifdef PY_OPENSSL_HAS_SCRYPT
1559+
// --- PBKDF: scrypt (RFC 7914) -----------------------------------------------
1560+
1561+
/*
1562+
* By default, OpenSSL 1.1.0 restricts 'maxmem' in EVP_PBE_scrypt()
1563+
* to 32 MiB (1024 * 1024 * 32) but only if 'maxmem = 0' and allows
1564+
* for an arbitrary large limit fitting on an uint64_t otherwise.
1565+
*
1566+
* For legacy reasons, we limited 'maxmem' to be at most INTMAX,
1567+
* but if users need a more relaxed value, we will revisit this
1568+
* limit in the future.
1569+
*/
1570+
#define HASHLIB_SCRYPT_MAX_MAXMEM INT_MAX
1571+
1572+
/*
1573+
* Limit 'dklen' to INT_MAX even if it can be at most (32 * UINT32_MAX).
1574+
*
1575+
* See https://datatracker.ietf.org/doc/html/rfc7914.html for details.
1576+
*/
1577+
#define HASHLIB_SCRYPT_MAX_DKLEN INT_MAX
15611578

15621579
/*[clinic input]
15631580
_hashlib.scrypt
@@ -1571,85 +1588,80 @@ _hashlib.scrypt
15711588
maxmem: long = 0
15721589
dklen: long = 64
15731590
1574-
15751591
scrypt password-based key derivation function.
15761592
[clinic start generated code]*/
15771593

15781594
static PyObject *
15791595
_hashlib_scrypt_impl(PyObject *module, Py_buffer *password, Py_buffer *salt,
15801596
unsigned long n, unsigned long r, unsigned long p,
15811597
long maxmem, long dklen)
1582-
/*[clinic end generated code: output=d424bc3e8c6b9654 input=0c9a84230238fd79]*/
1598+
/*[clinic end generated code: output=d424bc3e8c6b9654 input=bdeac9628d07f7a1]*/
15831599
{
1584-
PyObject *key_obj = NULL;
1585-
char *key;
1600+
PyObject *key = NULL;
15861601
int retval;
15871602

15881603
if (password->len > INT_MAX) {
1589-
PyErr_SetString(PyExc_OverflowError,
1590-
"password is too long.");
1604+
PyErr_SetString(PyExc_OverflowError, "password is too long");
15911605
return NULL;
15921606
}
15931607

15941608
if (salt->len > INT_MAX) {
1595-
PyErr_SetString(PyExc_OverflowError,
1596-
"salt is too long.");
1609+
PyErr_SetString(PyExc_OverflowError, "salt is too long");
15971610
return NULL;
15981611
}
15991612

16001613
if (n < 2 || n & (n - 1)) {
1601-
PyErr_SetString(PyExc_ValueError,
1602-
"n must be a power of 2.");
1614+
PyErr_SetString(PyExc_ValueError, "n must be a power of 2");
16031615
return NULL;
16041616
}
16051617

1606-
if (maxmem < 0 || maxmem > INT_MAX) {
1607-
/* OpenSSL 1.1.0 restricts maxmem to 32 MiB. It may change in the
1608-
future. The maxmem constant is private to OpenSSL. */
1618+
if (maxmem < 0 || maxmem > HASHLIB_SCRYPT_MAX_MAXMEM) {
16091619
PyErr_Format(PyExc_ValueError,
1610-
"maxmem must be positive and smaller than %d",
1611-
INT_MAX);
1620+
"maxmem must be positive and at most %d",
1621+
HASHLIB_SCRYPT_MAX_MAXMEM);
16121622
return NULL;
16131623
}
16141624

1615-
if (dklen < 1 || dklen > INT_MAX) {
1625+
if (dklen < 1 || dklen > HASHLIB_SCRYPT_MAX_DKLEN) {
16161626
PyErr_Format(PyExc_ValueError,
1617-
"dklen must be greater than 0 and smaller than %d",
1618-
INT_MAX);
1627+
"dklen must be at least 1 and at most %d",
1628+
HASHLIB_SCRYPT_MAX_DKLEN);
16191629
return NULL;
16201630
}
16211631

16221632
/* let OpenSSL validate the rest */
1623-
retval = EVP_PBE_scrypt(NULL, 0, NULL, 0, n, r, p, maxmem, NULL, 0);
1633+
retval = EVP_PBE_scrypt(NULL, 0, NULL, 0, n, r, p,
1634+
(uint64_t)maxmem, NULL, 0);
16241635
if (!retval) {
1625-
notify_ssl_error_occurred(
1626-
"Invalid parameter combination for n, r, p, maxmem.");
1636+
notify_ssl_error_occurred("invalid parameter combination for "
1637+
"n, r, p, and maxmem");
16271638
return NULL;
16281639
}
16291640

1630-
key_obj = PyBytes_FromStringAndSize(NULL, dklen);
1631-
if (key_obj == NULL) {
1641+
key = PyBytes_FromStringAndSize(NULL, dklen);
1642+
if (key == NULL) {
16321643
return NULL;
16331644
}
1634-
key = PyBytes_AS_STRING(key_obj);
16351645

16361646
Py_BEGIN_ALLOW_THREADS
16371647
retval = EVP_PBE_scrypt(
1638-
(const char*)password->buf, (size_t)password->len,
1648+
(const char *)password->buf, (size_t)password->len,
16391649
(const unsigned char *)salt->buf, (size_t)salt->len,
1640-
n, r, p, maxmem,
1641-
(unsigned char *)key, (size_t)dklen
1650+
(uint64_t)n, (uint64_t)r, (uint64_t)p, (uint64_t)maxmem,
1651+
(unsigned char *)PyBytes_AS_STRING(key), (size_t)dklen
16421652
);
16431653
Py_END_ALLOW_THREADS
16441654

16451655
if (!retval) {
1646-
Py_CLEAR(key_obj);
1656+
Py_DECREF(key);
16471657
notify_ssl_error_occurred_in(Py_STRINGIFY(EVP_PBE_scrypt));
16481658
return NULL;
16491659
}
1650-
return key_obj;
1660+
return key;
16511661
}
1652-
#endif /* PY_OPENSSL_HAS_SCRYPT */
1662+
1663+
#undef HASHLIB_SCRYPT_MAX_DKLEN
1664+
#undef HASHLIB_SCRYPT_MAX_MAXMEM
16531665

16541666
/* Fast HMAC for hmac.digest()
16551667
*/

Modules/clinic/_hashopenssl.c.h

Lines changed: 1 addition & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)