Repository: freedomofpress/securedrop

Issue:
Use python-cryptography in SecureDrop
# Feature request

## Description

[Cryptography](https://cryptography.io/en/latest/) is most used cryptographic module. During PyCon development sprints 2018, many security folks in the Python community suggested to use it inside SecureDrop than any other module.


## User Stories

I am a `SecureDrop` developer and I want to use properly audited code for cryptography in SecureDrop.


Discussion:

Resolving patch:
diff --git a/securedrop/crypto_util.py b/securedrop/crypto_util.py
--- a/securedrop/crypto_util.py
+++ b/securedrop/crypto_util.py
@@ -6,9 +6,9 @@
 import io
 import scrypt
 import subprocess
+from random import SystemRandom
 
 from base64 import b32encode
-from Cryptodome.Random import random
 from flask import current_app
 from gnupg._util import _is_stream, _make_binary_stream
 
@@ -24,6 +24,10 @@
 # to fix gpg error #78 on production
 os.environ['USERNAME'] = 'www-data'
 
+# SystemRandom sources from the system rand (e.g. urandom, CryptGenRandom, etc)
+# It supplies a CSPRNG but with an interface that supports methods like choice
+random = SystemRandom()
+
 
 class CryptoException(Exception):
     pass
diff --git a/securedrop/secure_tempfile.py b/securedrop/secure_tempfile.py
--- a/securedrop/secure_tempfile.py
+++ b/securedrop/secure_tempfile.py
@@ -5,9 +5,11 @@
 from tempfile import _TemporaryFileWrapper
 
 from gnupg._util import _STREAMLIKE_TYPES
-from Cryptodome.Cipher import AES
-from Cryptodome.Random import random
-from Cryptodome.Util import Counter
+from cryptography.exceptions import AlreadyFinalized
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.ciphers.algorithms import AES
+from cryptography.hazmat.primitives.ciphers.modes import CTR
+from cryptography.hazmat.primitives.ciphers import Cipher
 
 
 class SecureTemporaryFile(_TemporaryFileWrapper, object):
@@ -60,17 +62,16 @@ def create_key(self):
         https://github.com/freedomofpress/securedrop/pull/477#issuecomment-168445450).
         """
         self.key = os.urandom(self.AES_key_size / 8)
-        self.iv = random.getrandbits(self.AES_block_size)
+        self.iv = os.urandom(self.AES_block_size / 8)
         self.initialize_cipher()
 
     def initialize_cipher(self):
         """Creates the cipher-related objects needed for AES-CTR
         encryption and decryption.
         """
-        self.ctr_e = Counter.new(self.AES_block_size, initial_value=self.iv)
-        self.ctr_d = Counter.new(self.AES_block_size, initial_value=self.iv)
-        self.encryptor = AES.new(self.key, AES.MODE_CTR, counter=self.ctr_e)
-        self.decryptor = AES.new(self.key, AES.MODE_CTR, counter=self.ctr_d)
+        self.cipher = Cipher(AES(self.key), CTR(self.iv), default_backend())
+        self.encryptor = self.cipher.encryptor()
+        self.decryptor = self.cipher.decryptor()
 
     def write(self, data):
         """Write `data` to the secure temporary file. This method may be
@@ -85,7 +86,7 @@ def write(self, data):
         if isinstance(data, unicode):  # noqa
             data = data.encode('utf-8')
 
-        self.file.write(self.encryptor.encrypt(data))
+        self.file.write(self.encryptor.update(data))
 
     def read(self, count=None):
         """Read `data` from the secure temporary file. This method may
@@ -112,9 +113,23 @@ def read(self, count=None):
             self.last_action = 'read'
 
         if count:
-            return self.decryptor.decrypt(self.file.read(count))
+            return self.decryptor.update(self.file.read(count))
         else:
-            return self.decryptor.decrypt(self.file.read())
+            return self.decryptor.update(self.file.read())
+
+    def close(self):
+        """The __del__ method in tempfile._TemporaryFileWrapper (which
+        SecureTemporaryFile class inherits from) calls close() when the
+        temporary file is deleted.
+        """
+        try:
+            self.decryptor.finalize()
+        except AlreadyFinalized:
+            pass
+
+        # Since tempfile._TemporaryFileWrapper.close() does other cleanup,
+        # (i.e. deleting the temp file on disk), we need to call it also.
+        super(SecureTemporaryFile, self).close()
 
 
 # python-gnupg will not recognize our SecureTemporaryFile as a stream-like type


Test patch:
diff --git a/securedrop/tests/functional/functional_test.py b/securedrop/tests/functional/functional_test.py
--- a/securedrop/tests/functional/functional_test.py
+++ b/securedrop/tests/functional/functional_test.py
@@ -9,7 +9,6 @@
 import traceback
 import requests
 
-from Cryptodome import Random
 from datetime import datetime
 from multiprocessing import Process
 from os.path import abspath, dirname, join, realpath
@@ -113,14 +112,6 @@ def setup(self, session_expiration=30):
         self.journalist_app = journalist_app.create_app(config)
 
         def start_source_server(app):
-            # We call Random.atfork() here because we fork the source and
-            # journalist server from the main Python process we use to drive
-            # our browser with multiprocessing.Process() below. These child
-            # processes inherit the same RNG state as the parent process, which
-            # is a problem because they would produce identical output if we
-            # didn't re-seed them after forking.
-            Random.atfork()
-
             config.SESSION_EXPIRATION_MINUTES = self.session_expiration
 
             app.run(
@@ -130,7 +121,6 @@ def start_source_server(app):
                 threaded=True)
 
         def start_journalist_server(app):
-            Random.atfork()
             app.run(
                 port=journalist_port,
                 debug=True,