[Python-checkins] cpython: Issue #27928: Add scrypt (password-based key derivation function) to hashlib

christian.heimes python-checkins at python.org
Tue Sep 6 14:23:55 EDT 2016


https://hg.python.org/cpython/rev/d926fa1a833c
changeset:   103127:d926fa1a833c
user:        Christian Heimes <christian at python.org>
date:        Tue Sep 06 20:22:28 2016 +0200
summary:
  Issue #27928: Add scrypt (password-based key derivation function) to hashlib module (requires OpenSSL 1.1.0).

files:
  Doc/library/hashlib.rst         |   17 ++
  Lib/hashlib.py                  |    6 +
  Lib/test/test_hashlib.py        |   47 +++++++
  Misc/NEWS                       |    3 +
  Modules/_hashopenssl.c          |  129 ++++++++++++++++++++
  Modules/clinic/_hashopenssl.c.h |   60 +++++++++
  6 files changed, 262 insertions(+), 0 deletions(-)


diff --git a/Doc/library/hashlib.rst b/Doc/library/hashlib.rst
--- a/Doc/library/hashlib.rst
+++ b/Doc/library/hashlib.rst
@@ -225,6 +225,23 @@
       Python implementation uses an inline version of :mod:`hmac`. It is about
       three times slower and doesn't release the GIL.
 
+.. function:: scrypt(password, *, salt, n, r, p, maxmem=0, dklen=64)
+
+   The function provides scrypt password-based key derivation function as
+   defined in :rfc:`7914`.
+
+   *password* and *salt* must be bytes-like objects. Applications and
+   libraries should limit *password* to a sensible length (e.g. 1024). *salt*
+   should be about 16 or more bytes from a proper source, e.g. :func:`os.urandom`.
+
+   *n* is the CPU/Memory cost factor, *r* the block size, *p* parallelization
+   factor and *maxmem* limits memory (OpenSSL 1.1.0 defaults to 32 MB).
+   *dklen* is the length of the derived key.
+
+   Availability: OpenSSL 1.1+
+
+   .. versionadded:: 3.6
+
 
 .. seealso::
 
diff --git a/Lib/hashlib.py b/Lib/hashlib.py
--- a/Lib/hashlib.py
+++ b/Lib/hashlib.py
@@ -202,6 +202,12 @@
 
         return dkey[:dklen]
 
+try:
+    # OpenSSL's scrypt requires OpenSSL 1.1+
+    from _hashlib import scrypt
+except ImportError:
+    pass
+
 
 for __func_name in __always_supported:
     # try them all, some may not work due to the OpenSSL
diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py
--- a/Lib/test/test_hashlib.py
+++ b/Lib/test/test_hashlib.py
@@ -7,6 +7,7 @@
 #
 
 import array
+from binascii import unhexlify
 import hashlib
 import itertools
 import os
@@ -447,6 +448,12 @@
         (b'pass\0word', b'sa\0lt', 4096, 16),
     ]
 
+    scrypt_test_vectors = [
+        (b'', b'', 16, 1, 1, unhexlify('77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906')),
+        (b'password', b'NaCl', 1024, 8, 16, unhexlify('fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640')),
+        (b'pleaseletmein', b'SodiumChloride', 16384, 8, 1, unhexlify('7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887')),
+   ]
+
     pbkdf2_results = {
         "sha1": [
             # official test vectors from RFC 6070
@@ -526,5 +533,45 @@
         self._test_pbkdf2_hmac(c_hashlib.pbkdf2_hmac)
 
 
+    @unittest.skipUnless(hasattr(c_hashlib, 'scrypt'),
+                     '   test requires OpenSSL > 1.1')
+    def test_scrypt(self):
+        for password, salt, n, r, p, expected in self.scrypt_test_vectors:
+            result = hashlib.scrypt(password, salt=salt, n=n, r=r, p=p)
+            self.assertEqual(result, expected)
+
+        # this values should work
+        hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1)
+        # password and salt must be bytes-like
+        with self.assertRaises(TypeError):
+            hashlib.scrypt('password', salt=b'salt', n=2, r=8, p=1)
+        with self.assertRaises(TypeError):
+            hashlib.scrypt(b'password', salt='salt', n=2, r=8, p=1)
+        # require keyword args
+        with self.assertRaises(TypeError):
+            hashlib.scrypt(b'password')
+        with self.assertRaises(TypeError):
+            hashlib.scrypt(b'password', b'salt')
+        with self.assertRaises(TypeError):
+            hashlib.scrypt(b'password', 2, 8, 1, salt=b'salt')
+        for n in [-1, 0, 1, None]:
+            with self.assertRaises((ValueError, OverflowError, TypeError)):
+                hashlib.scrypt(b'password', salt=b'salt', n=n, r=8, p=1)
+        for r in [-1, 0, None]:
+            with self.assertRaises((ValueError, OverflowError, TypeError)):
+                hashlib.scrypt(b'password', salt=b'salt', n=2, r=r, p=1)
+        for p in [-1, 0, None]:
+            with self.assertRaises((ValueError, OverflowError, TypeError)):
+                hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=p)
+        for maxmem in [-1, None]:
+            with self.assertRaises((ValueError, OverflowError, TypeError)):
+                hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1,
+                               maxmem=maxmem)
+        for dklen in [-1, None]:
+            with self.assertRaises((ValueError, OverflowError, TypeError)):
+                hashlib.scrypt(b'password', salt=b'salt', n=2, r=8, p=1,
+                               dklen=dklen)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -85,6 +85,9 @@
 Library
 -------
 
+- Issue #27928: Add scrypt (password-based key derivation function) to
+  hashlib module (requires OpenSSL 1.1.0).
+
 - Issue #27850: Remove 3DES from ssl module's default cipher list to counter
   measure sweet32 attack (CVE-2016-2183).
 
diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c
--- a/Modules/_hashopenssl.c
+++ b/Modules/_hashopenssl.c
@@ -25,6 +25,12 @@
 #include <openssl/objects.h>
 #include "openssl/err.h"
 
+#include "clinic/_hashopenssl.c.h"
+/*[clinic input]
+module _hashlib
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=c2b4ff081bac4be1]*/
+
 #define MUNCH_SIZE INT_MAX
 
 #ifndef HASH_OBJ_CONSTRUCTOR
@@ -713,6 +719,128 @@
 
 #endif
 
+#if OPENSSL_VERSION_NUMBER > 0x10100000L && !defined(OPENSSL_NO_SCRYPT) && !defined(LIBRESSL_VERSION_NUMBER)
+#define PY_SCRYPT 1
+
+/*[clinic input]
+_hashlib.scrypt
+
+    password: Py_buffer
+    *
+    salt: Py_buffer = None
+    n as n_obj: object(subclass_of='&PyLong_Type') = None
+    r as r_obj: object(subclass_of='&PyLong_Type') = None
+    p as p_obj: object(subclass_of='&PyLong_Type') = None
+    maxmem: long = 0
+    dklen: long = 64
+
+
+scrypt password-based key derivation function.
+[clinic start generated code]*/
+
+static PyObject *
+_hashlib_scrypt_impl(PyObject *module, Py_buffer *password, Py_buffer *salt,
+                     PyObject *n_obj, PyObject *r_obj, PyObject *p_obj,
+                     long maxmem, long dklen)
+/*[clinic end generated code: output=14849e2aa2b7b46c input=48a7d63bf3f75c42]*/
+{
+    PyObject *key_obj = NULL;
+    char *key;
+    int retval;
+    unsigned long n, r, p;
+
+    if (password->len > INT_MAX) {
+        PyErr_SetString(PyExc_OverflowError,
+                        "password is too long.");
+        return NULL;
+    }
+
+    if (salt->buf == NULL) {
+        PyErr_SetString(PyExc_TypeError,
+                        "salt is required");
+        return NULL;
+    }
+    if (salt->len > INT_MAX) {
+        PyErr_SetString(PyExc_OverflowError,
+                        "salt is too long.");
+        return NULL;
+    }
+
+    n = PyLong_AsUnsignedLong(n_obj);
+    if (n == (unsigned long) -1 && PyErr_Occurred()) {
+        PyErr_SetString(PyExc_TypeError,
+                        "n is required and must be an unsigned int");
+        return NULL;
+    }
+    if (n < 2 || n & (n - 1)) {
+        PyErr_SetString(PyExc_ValueError,
+                        "n must be a power of 2.");
+        return NULL;
+    }
+
+    r = PyLong_AsUnsignedLong(r_obj);
+    if (r == (unsigned long) -1 && PyErr_Occurred()) {
+        PyErr_SetString(PyExc_TypeError,
+                         "r is required and must be an unsigned int");
+        return NULL;
+    }
+
+    p = PyLong_AsUnsignedLong(p_obj);
+    if (p == (unsigned long) -1 && PyErr_Occurred()) {
+        PyErr_SetString(PyExc_TypeError,
+                         "p is required and must be an unsigned int");
+        return NULL;
+    }
+
+    if (maxmem < 0 || maxmem > INT_MAX) {
+        /* OpenSSL 1.1.0 restricts maxmem to 32MB. It may change in the
+           future. The maxmem constant is private to OpenSSL. */
+        PyErr_Format(PyExc_ValueError,
+                     "maxmem must be positive and smaller than %d",
+                      INT_MAX);
+        return NULL;
+    }
+
+    if (dklen < 1 || dklen > INT_MAX) {
+        PyErr_Format(PyExc_ValueError,
+                    "dklen must be greater than 0 and smaller than %d",
+                    INT_MAX);
+        return NULL;
+    }
+
+    /* let OpenSSL validate the rest */
+    retval = EVP_PBE_scrypt(NULL, 0, NULL, 0, n, r, p, maxmem, NULL, 0);
+    if (!retval) {
+        /* sorry, can't do much better */
+        PyErr_SetString(PyExc_ValueError,
+                        "Invalid paramemter combination for n, r, p, maxmem.");
+        return NULL;
+   }
+
+    key_obj = PyBytes_FromStringAndSize(NULL, dklen);
+    if (key_obj == NULL) {
+        return NULL;
+    }
+    key = PyBytes_AS_STRING(key_obj);
+
+    Py_BEGIN_ALLOW_THREADS
+    retval = EVP_PBE_scrypt(
+        (const char*)password->buf, (size_t)password->len,
+        (const unsigned char *)salt->buf, (size_t)salt->len,
+        n, r, p, maxmem,
+        (unsigned char *)key, (size_t)dklen
+    );
+    Py_END_ALLOW_THREADS
+
+    if (!retval) {
+        Py_CLEAR(key_obj);
+        _setException(PyExc_ValueError);
+        return NULL;
+    }
+    return key_obj;
+}
+#endif
+
 /* State for our callback function so that it can accumulate a result. */
 typedef struct _internal_name_mapper_state {
     PyObject *set;
@@ -836,6 +964,7 @@
     {"pbkdf2_hmac", (PyCFunction)pbkdf2_hmac, METH_VARARGS|METH_KEYWORDS,
      pbkdf2_hmac__doc__},
 #endif
+    _HASHLIB_SCRYPT_METHODDEF
     CONSTRUCTOR_METH_DEF(md5),
     CONSTRUCTOR_METH_DEF(sha1),
     CONSTRUCTOR_METH_DEF(sha224),
diff --git a/Modules/clinic/_hashopenssl.c.h b/Modules/clinic/_hashopenssl.c.h
new file mode 100644
--- /dev/null
+++ b/Modules/clinic/_hashopenssl.c.h
@@ -0,0 +1,60 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#if (OPENSSL_VERSION_NUMBER > 0x10100000L && !defined(OPENSSL_NO_SCRYPT) && !defined(LIBRESSL_VERSION_NUMBER))
+
+PyDoc_STRVAR(_hashlib_scrypt__doc__,
+"scrypt($module, /, password, *, salt=None, n=None, r=None, p=None,\n"
+"       maxmem=0, dklen=64)\n"
+"--\n"
+"\n"
+"scrypt password-based key derivation function.");
+
+#define _HASHLIB_SCRYPT_METHODDEF    \
+    {"scrypt", (PyCFunction)_hashlib_scrypt, METH_VARARGS|METH_KEYWORDS, _hashlib_scrypt__doc__},
+
+static PyObject *
+_hashlib_scrypt_impl(PyObject *module, Py_buffer *password, Py_buffer *salt,
+                     PyObject *n_obj, PyObject *r_obj, PyObject *p_obj,
+                     long maxmem, long dklen);
+
+static PyObject *
+_hashlib_scrypt(PyObject *module, PyObject *args, PyObject *kwargs)
+{
+    PyObject *return_value = NULL;
+    static const char * const _keywords[] = {"password", "salt", "n", "r", "p", "maxmem", "dklen", NULL};
+    static _PyArg_Parser _parser = {"y*|$y*O!O!O!ll:scrypt", _keywords, 0};
+    Py_buffer password = {NULL, NULL};
+    Py_buffer salt = {NULL, NULL};
+    PyObject *n_obj = Py_None;
+    PyObject *r_obj = Py_None;
+    PyObject *p_obj = Py_None;
+    long maxmem = 0;
+    long dklen = 64;
+
+    if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &_parser,
+        &password, &salt, &PyLong_Type, &n_obj, &PyLong_Type, &r_obj, &PyLong_Type, &p_obj, &maxmem, &dklen)) {
+        goto exit;
+    }
+    return_value = _hashlib_scrypt_impl(module, &password, &salt, n_obj, r_obj, p_obj, maxmem, dklen);
+
+exit:
+    /* Cleanup for password */
+    if (password.obj) {
+       PyBuffer_Release(&password);
+    }
+    /* Cleanup for salt */
+    if (salt.obj) {
+       PyBuffer_Release(&salt);
+    }
+
+    return return_value;
+}
+
+#endif /* (OPENSSL_VERSION_NUMBER > 0x10100000L && !defined(OPENSSL_NO_SCRYPT) && !defined(LIBRESSL_VERSION_NUMBER)) */
+
+#ifndef _HASHLIB_SCRYPT_METHODDEF
+    #define _HASHLIB_SCRYPT_METHODDEF
+#endif /* !defined(_HASHLIB_SCRYPT_METHODDEF) */
+/*[clinic end generated code: output=8c5386789f77430a input=a9049054013a1b77]*/

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list