Repository: ros/ros_comm

Issue:
roscomm/ros_bag crypto implementation
Hello,
after doing a manual install of ros (Melodic Morenia) on CentOS 7, I found a few issues in bag.py:
from Crypto import Random
from Crypto.Cipher import AES
Those routines are provided by pycrypto, which is unmaintained since 2013. Its replacement is cryptodome, which can be installed (https://pycryptodome.readthedocs.io/en/latest/src/installation.html) either as drop-in replacement, either as package "Cryptodome". The latter approach was used in Centos, so bag.py needs the following patch:
--- bag.py.orig	2019-01-30 13:46:25.700767679 +0100
+++ bag.py	2019-01-30 13:48:02.589286201 +0100
@@ -50,8 +50,13 @@
 import time
 import yaml
 
-from Crypto import Random
-from Crypto.Cipher import AES
+try:
+	from Crypto import Random
+	from Crypto.Cipher import AES
+except ImportError:
+	from Cryptodome import Random
+	from Cryptodome.Cipher import AES
+
 import gnupg

This is the "least surprise" approach: use pycrypto first, and cryptodome if not found.

Yet there are more issues lurking around. Around line 221:
        f.seek(chunk_data_pos)
         chunk = _read(f, chunk_size)
         # Encrypt chunk
        iv = Random.new().read(AES.block_size)
        f.seek(chunk_data_pos)
        f.write(iv)
pycryto uses its own entropy source, while cryptodome uses the correct os.urandom() approach. By comparing sources, its seems that pycrypto never called urandom and was subject to race conditions (CVE-2013-1445). The author of both modules is the same person, yet the evolution favored the dropping of the own implementation towards the use of the system-level random generator.

Would it be allowable to 
1) favor cryptodome over crypto
2) replace all lines like:
iv = Random.new().read(AES.block_size)
by direct call:
iv = os.urandom(AES.block_size)
to remove the extraneous interface layer of the Random class and ensure the right function is called?

Regards

Pascal




Discussion:
This ticket replaces ros/ros#201.

Please create a pull request with the proposed patch.

Resolving patch:
diff --git a/tools/rosbag/src/rosbag/bag.py b/tools/rosbag/src/rosbag/bag.py
--- a/tools/rosbag/src/rosbag/bag.py
+++ b/tools/rosbag/src/rosbag/bag.py
@@ -50,8 +50,9 @@
 import time
 import yaml
 
-from Crypto import Random
-from Crypto.Cipher import AES
+from Cryptodome.Cipher import AES
+from Cryptodome.Random import random
+
 import gnupg
 
 try:
@@ -221,7 +222,7 @@ def encrypt_chunk(self, chunk_size, chunk_data_pos, f):
         f.seek(chunk_data_pos)
         chunk = _read(f, chunk_size)
         # Encrypt chunk
-        iv = Random.new().read(AES.block_size)
+        iv = random.getrandbits(AES.block_size)
         f.seek(chunk_data_pos)
         f.write(iv)
         cipher = AES.new(self._symmetric_key, AES.MODE_CBC, iv)
@@ -299,7 +300,7 @@ def write_encrypted_header(self, _, f, header):
                 v = v.encode()
             header_str += _pack_uint32(len(k) + 1 + len(v)) + k + equal + v
 
-        iv = Random.new().read(AES.block_size)
+        iv = random.getrandbits(AES.block_size)
         enc_str = iv
         cipher = AES.new(self._symmetric_key, AES.MODE_CBC, iv)
         enc_str += cipher.encrypt(_add_padding(header_str))
@@ -345,7 +346,7 @@ def get_info_str(self):
     def _build_symmetric_key(self):
         if not self._gpg_key_user:
             return
-        self._symmetric_key = Random.new().read(AES.block_size)
+        self._symmetric_key = random.getrandbits(AES.block_size)
         self._encrypted_symmetric_key = _encrypt_string_gpg(self._gpg_key_user, self._symmetric_key)
 
     def _decrypt_encrypted_header(self, f):


Test patch: