Repository: pyca/cryptography

Issue:
Raise MemoryError when Scrypt can't malloc enough memory
When someone makes a Scrypt derivation call and the system doesn't have enough memory to `malloc`, it raises quite a noisey OpenSSL `InternalError`.

```
InternalError: Unknown OpenSSL error. This error is commonly encountered when another library is not cleaning up the OpenSSL error stack. If you are using cryptography with another library that uses OpenSSL try disabling it before reporting a bug. Otherwise please file an issue at https://github.com/pyca/cryptography/issues with information on how to reproduce this. ([_OpenSSLErrorWithText(code=101404737, lib=6, func=181, reason=65, reason_text=b'error:060B5041:digital envelope routines:EVP_PBE_scrypt:malloc failure')])
(14 additional frame(s) were not displayed)
...
  File "nucypher/config/keyring.py", line 191, in _derive_key_material_from_passphrase
    ).derive(passphrase.encode())
  File "cryptography/hazmat/primitives/kdf/scrypt.py", line 60, in derive
    key_material, self._salt, self._length, self._n, self._r, self._p
  File "cryptography/hazmat/backends/openssl/backend.py", line 1950, in derive_scrypt
    self.openssl_assert(res == 1)
  File "cryptography/hazmat/backends/openssl/backend.py", line 105, in openssl_assert
    return binding._openssl_assert(self._lib, ok)
  File "cryptography/hazmat/bindings/openssl/binding.py", line 76, in _openssl_assert
    errors_with_text
```

Would it make sense to catch that and raise `MemoryError`, instead?


Discussion:
Yes, this would be a good imprveoment.
I'll go ahead and take it, then. :)

Resolving patch:
diff --git a/src/_cffi_src/openssl/err.py b/src/_cffi_src/openssl/err.py
--- a/src/_cffi_src/openssl/err.py
+++ b/src/_cffi_src/openssl/err.py
@@ -22,6 +22,8 @@
 static const int ERR_LIB_SSL;
 static const int ERR_LIB_X509;
 
+static const int ERR_R_MALLOC_FAILURE;
+
 static const int ASN1_R_BOOLEAN_IS_WRONG_LENGTH;
 static const int ASN1_R_BUFFER_TOO_SMALL;
 static const int ASN1_R_CIPHER_HAS_NO_OBJECT_IDENTIFIER;
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -2086,7 +2086,24 @@ def derive_scrypt(self, key_material, salt, length, n, r, p):
             key_material, len(key_material), salt, len(salt), n, r, p,
             scrypt._MEM_LIMIT, buf, length
         )
-        self.openssl_assert(res == 1)
+        if res != 1:
+            errors = self._consume_errors()
+            if not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111:
+                # This error is only added to the stack in 1.1.1+
+                self.openssl_assert(
+                    errors[0]._lib_reason_match(
+                        self._lib.ERR_LIB_EVP,
+                        self._lib.ERR_R_MALLOC_FAILURE
+                    )
+                )
+
+            # memory required formula explained here:
+            # https://blog.filippo.io/the-scrypt-parameters/
+            min_memory = 128 * n * r // (1024**2)
+            raise MemoryError(
+                "Not enough memory to derive key. These parameters require"
+                " {} MB of memory.".format(min_memory)
+            )
         return self._ffi.buffer(buf)[:]
 
     def aead_cipher_supported(self, cipher):


Test patch:
diff --git a/tests/hazmat/primitives/test_scrypt.py b/tests/hazmat/primitives/test_scrypt.py
--- a/tests/hazmat/primitives/test_scrypt.py
+++ b/tests/hazmat/primitives/test_scrypt.py
@@ -80,6 +80,20 @@ def test_salt_not_bytes(self, backend):
             Scrypt(salt, length, work_factor, block_size,
                    parallelization_factor, backend)
 
+    def test_scrypt_malloc_failure(self, backend):
+        password = b"NaCl"
+        work_factor = 1024 ** 3
+        block_size = 589824
+        parallelization_factor = 16
+        length = 64
+        salt = b"NaCl"
+
+        scrypt = Scrypt(salt, length, work_factor, block_size,
+                        parallelization_factor, backend)
+
+        with pytest.raises(MemoryError):
+            scrypt.derive(password)
+
     def test_password_not_bytes(self, backend):
         password = 1
         work_factor = 1024