[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