[packages/python3-crypt-r] - add METHOD_YESCRYPT (salt from libxcrypt crypt_gensalt), rel 4
arekm
arekm at pld-linux.org
Tue Oct 13 15:30:35 CEST 2026
commit 70df0e5306a25d782944b4725ab5d78ca987201d
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Tue Oct 13 15:25:12 2026 +0200
- add METHOD_YESCRYPT (salt from libxcrypt crypt_gensalt), rel 4
python3-crypt-r.spec | 5 +-
yescrypt.patch | 214 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 218 insertions(+), 1 deletion(-)
---
diff --git a/python3-crypt-r.spec b/python3-crypt-r.spec
index 1597892..553971d 100644
--- a/python3-crypt-r.spec
+++ b/python3-crypt-r.spec
@@ -5,12 +5,14 @@
Summary: A copy of the `crypt` module that was removed in Python 3.13
Name: python3-crypt-r
Version: 3.13.1
-Release: 3
+Release: 4
License: PSL
Group: Libraries/Python
Source0: https://files.pythonhosted.org/packages/source/c/crypt-r/%{module}-%{version}.tar.gz
# Source0-md5: 8f37eed58cb8ada45721b703432b819f
+Patch0: yescrypt.patch
URL: https://pypi.org/project/crypt-r/
+BuildRequires: libxcrypt-devel >= 4.2.0
BuildRequires: python3-build
BuildRequires: python3-devel >= 1:3.2
BuildRequires: python3-installer
@@ -31,6 +33,7 @@ See PEP 594 for details of the removal.
%prep
%setup -q -n %{module}-%{version}
+%patch -P0 -p1
%build
%py3_build_pyproject
diff --git a/yescrypt.patch b/yescrypt.patch
new file mode 100644
index 0000000..4bd2673
--- /dev/null
+++ b/yescrypt.patch
@@ -0,0 +1,214 @@
+Add METHOD_YESCRYPT: crypt_gensalt(3) from libxcrypt builds the setting
+string (yescrypt needs 16 bytes in its own base64 encoding, so random
+characters from _saltchars are rejected); rounds maps to the libxcrypt
+cost 1-11. yescrypt becomes the first (default) method, as in libxcrypt.
+
+diff --git a/README.rst b/README.rst
+index 834720b..4c75eda 100644
+--- a/README.rst
++++ b/README.rst
+@@ -21,8 +21,6 @@ not security/cryptography.
+ It is easy to use ``crypt_r`` in an insecure way. Notably:
+ All hashing methods except ``METHOD_CRYPT`` (the original Unix algorithm from the 1970s)
+ are optional platform-specific extensions.
+-This library does not expose modern hashing methods like libxcrypt's yescrypt.
+-The last wrapper update is from 2017.
+ No future development is planned.
+
+ To use this module, you can either import ``crypt_r`` explicitly
+@@ -54,9 +52,15 @@ New in Python 3.3.
+ The ``crypt_r`` module defines the list of hashing methods (not all methods
+ are available on all platforms):
+
++``METHOD_YESCRYPT``
++ A Modular Crypt Format method with 22 character salt and 43 character
++ hash based on the yescrypt key derivation function; the default and
++ recommended method of libxcrypt. The salt is generated by
++ ``crypt_gensalt(3)`` from libxcrypt. This is the strongest method.
++
+ ``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.
+
+ ``METHOD_SHA256``
+ Another Modular Crypt Format method with 16 character salt and 43
+@@ -131,12 +135,15 @@ The ``crypt_r`` module defines the following functions:
+ to ``crypt`` .
+
+ *rounds* specifies the number of rounds for ``METHOD_SHA256``,
+- ``METHOD_SHA512`` and ``METHOD_BLOWFISH``.
++ ``METHOD_SHA512`` and ``METHOD_BLOWFISH``, and the cost for
++ ``METHOD_YESCRYPT``.
+ For ``METHOD_SHA256`` and ``METHOD_SHA512`` it must be an integer between
+ ``1000`` and ``999_999_999``, the default is ``5000``. For
+ ``METHOD_BLOWFISH`` it must be a power of two between ``16`` (2\ :sup:`4`)
+ and ``2_147_483_648`` (2\ :sup:`31`), the default is ``4096``
+- (2\ :sup:`12`).
++ (2\ :sup:`12`). For ``METHOD_YESCRYPT`` it is libxcrypt's cost
++ parameter, an integer between ``1`` and ``11``; the default is chosen
++ by libxcrypt.
+
+ New in Python 3.3.
+
+diff --git a/src/_crypt_r.c b/src/_crypt_r.c
+index 066ed53..0731c44 100644
+--- a/src/_crypt_r.c
++++ b/src/_crypt_r.c
+@@ -82,8 +82,68 @@ crypt_r_crypt(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+ }
+
+
++PyDoc_STRVAR(crypt_r_crypt_gensalt__doc__,
++"crypt_gensalt($module, prefix, count, /)\n"
++"--\n"
++"\n"
++"Return a setting string for the hashing method selected by *prefix*\n"
++"(e.g. \"$y$\") with cost *count* (0 selects the method's default),\n"
++"salted with random bytes from the operating system.\n"
++"\n"
++"Raises OSError with errno ENOSYS if the crypt library cannot generate\n"
++"salts on its own.");
++
++static PyObject *
++crypt_r_crypt_gensalt(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
++{
++ if (nargs != 2) {
++ PyErr_Format(PyExc_TypeError, "crypt_gensalt expected 2 arguments, got %zd", nargs);
++ return NULL;
++ }
++ const char *prefix;
++ Py_ssize_t prefix_length;
++ unsigned long count;
++
++ if (!PyUnicode_Check(args[0])) {
++ PyErr_Format(
++ PyExc_TypeError, "crypt_gensalt argument 1 (prefix) must be string, not %s",
++ Py_TYPE(args[0])->tp_name
++ );
++ return NULL;
++ }
++ prefix = PyUnicode_AsUTF8AndSize(args[0], &prefix_length);
++ if (prefix == NULL) {
++ return NULL;
++ }
++ if (strlen(prefix) != (size_t)prefix_length) {
++ PyErr_SetString(
++ PyExc_ValueError,
++ "crypt_gensalt argument 1 (prefix) contains embedded null character"
++ );
++ return NULL;
++ }
++
++ count = PyLong_AsUnsignedLong(args[1]);
++ if (count == (unsigned long)-1 && PyErr_Occurred()) {
++ return NULL;
++ }
++
++#ifdef CRYPT_GENSALT_IMPLEMENTS_AUTO_ENTROPY
++ char output[CRYPT_GENSALT_OUTPUT_SIZE];
++ if (crypt_gensalt_rn(prefix, count, NULL, 0, output, sizeof(output)) == NULL) {
++ return PyErr_SetFromErrno(PyExc_OSError);
++ }
++ return PyUnicode_FromString(output);
++#else
++ errno = ENOSYS;
++ return PyErr_SetFromErrno(PyExc_OSError);
++#endif
++}
++
++
+ static PyMethodDef crypt_r_methods[] = {
+ {"crypt", (PyCFunction)crypt_r_crypt, METH_FASTCALL, crypt_r_crypt__doc__},
++ {"crypt_gensalt", (PyCFunction)crypt_r_crypt_gensalt, METH_FASTCALL, crypt_r_crypt_gensalt__doc__},
+ {NULL, NULL} /* sentinel */
+ };
+
+diff --git a/src/crypt_r.py b/src/crypt_r.py
+index afd6d82..948bdd1 100644
+--- a/src/crypt_r.py
++++ b/src/crypt_r.py
+@@ -45,6 +45,15 @@ def mksalt(method=None, *, rounds=None):
+ else: # modular
+ s = f'${method.ident}$'
+
++ if method.ident == 'y': # yescrypt
++ # The salt must be 16 bytes in yescrypt's own base64 encoding, and the
++ # cost is encoded into the parameters: let libxcrypt build both.
++ if rounds is None:
++ rounds = 0 # libxcrypt's default cost
++ elif not 1 <= rounds <= 11:
++ raise ValueError('rounds out of the range 1 to 11')
++ return _crypt_r.crypt_gensalt(s, rounds)
++
+ if method.ident and method.ident[0] == '2': # Blowfish variants
+ if rounds is None:
+ log_rounds = 12
+@@ -88,9 +97,9 @@ methods = []
+ def _add_method(name, *args, rounds=None):
+ method = _Method(name, *args)
+ globals()['METHOD_' + name] = method
+- salt = mksalt(method, rounds=rounds)
+ result = None
+ try:
++ salt = mksalt(method, rounds=rounds)
+ result = crypt('', salt)
+ except OSError as e:
+ # Not all libc libraries support all encryption methods.
+@@ -102,6 +111,8 @@ def _add_method(name, *args, rounds=None):
+ return True
+ return False
+
++# libxcrypt's default and recommended method for new hashes.
++_add_method('YESCRYPT', 'y', 22, 73)
+ _add_method('SHA512', '6', 16, 106)
+ _add_method('SHA256', '5', 16, 63)
+
+diff --git a/tests/test_crypt_r.py b/tests/test_crypt_r.py
+index ef8769e..b5e26f5 100644
+--- a/tests/test_crypt_r.py
++++ b/tests/test_crypt_r.py
+@@ -69,7 +69,43 @@ class CryptRTestCase(unittest.TestCase):
+ cr2 = self.crypt.crypt('mypassword', cr)
+ self.assertEqual(cr2, cr)
+
++ @unittest.skipUnless(
++ crypt_r.METHOD_YESCRYPT in crypt_r.methods, 'requires support of yescrypt'
++ )
++ def test_yescrypt(self):
++ self.assertEqual(self.crypt.methods[0], self.crypt.METHOD_YESCRYPT)
++ # Arbitrary characters from _saltchars are not a valid yescrypt salt,
++ # so every generated one must be accepted, not just most of them.
++ salts = {self.crypt.mksalt(self.crypt.METHOD_YESCRYPT) for _ in range(20)}
++ self.assertEqual(len(salts), 20)
++ for salt in salts:
++ cr = self.crypt.crypt('mypassword', salt)
++ self.assertEqual(len(cr), self.crypt.METHOD_YESCRYPT.total_size)
++ self.assertTrue(cr.startswith(salt + '$'))
++ self.assertEqual(self.crypt.crypt('mypassword', cr), cr)
++ cr = self.crypt.crypt('mypassword', self.crypt.METHOD_YESCRYPT)
++ self.assertNotEqual(self.crypt.crypt('wrongpassword', cr), cr)
++
++ @unittest.skipUnless(
++ crypt_r.METHOD_YESCRYPT in crypt_r.methods, 'requires support of yescrypt'
++ )
++ def test_yescrypt_rounds(self):
++ params = set()
++ for rounds in range(1, 6):
++ salt = self.crypt.mksalt(self.crypt.METHOD_YESCRYPT, rounds=rounds)
++ self.assertTrue(salt.startswith('$y$'))
++ params.add(salt[:-self.crypt.METHOD_YESCRYPT.salt_chars])
++ cr = self.crypt.crypt('mypassword', salt)
++ self.assertTrue(cr.startswith(salt + '$'))
++ self.assertEqual(self.crypt.crypt('mypassword', cr), cr)
++ self.assertEqual(len(params), 5)
++
+ def test_invalid_rounds(self):
++ with self.assertRaises(TypeError):
++ self.crypt.mksalt(self.crypt.METHOD_YESCRYPT, rounds='5')
++ for rounds in (0, 12, -1, 1<<999):
++ with self.assertRaises(ValueError):
++ self.crypt.mksalt(self.crypt.METHOD_YESCRYPT, rounds=rounds)
+ for method in (self.crypt.METHOD_SHA256, self.crypt.METHOD_SHA512,
+ self.crypt.METHOD_BLOWFISH):
+ with self.assertRaises(TypeError):
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/packages/python3-crypt-r.git/commitdiff/70df0e5306a25d782944b4725ab5d78ca987201d
More information about the pld-cvs-commit
mailing list