Issue #27928: Add scrypt (password-based key derivation function) to hashlib module (requires OpenSSL 1.1.0).

This commit is contained in:
Christian Heimes 2016-09-06 20:22:28 +02:00
parent ac041c0aa7
commit 39093e9e68
6 changed files with 262 additions and 0 deletions

View File

@ -225,6 +225,23 @@ include a `salt <https://en.wikipedia.org/wiki/Salt_%28cryptography%29>`_.
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::

View File

@ -202,6 +202,12 @@ except ImportError:
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

View File

@ -7,6 +7,7 @@
#
import array
from binascii import unhexlify
import hashlib
import itertools
import os
@ -447,6 +448,12 @@ class KDFTests(unittest.TestCase):
(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 @@ class KDFTests(unittest.TestCase):
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()

View File

@ -85,6 +85,9 @@ Core and Builtins
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).

View File

@ -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 @@ pbkdf2_hmac(PyObject *self, PyObject *args, PyObject *kwdict)
#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 @@ static struct PyMethodDef EVP_functions[] = {
{"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),

View File

@ -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]*/