Skip to content

Commit cea2675

Browse files
author
Allie Crevier
committed
address pr comments
1 parent 18d85a6 commit cea2675

File tree

5 files changed

+85
-64
lines changed

5 files changed

+85
-64
lines changed

securedrop_client/export.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ class Export:
4343
securedrop-export repository.
4444
'''
4545

46-
QVM_OP = 'qvm-open-in-vm'
47-
QVM_VM = 'sd-export-usb'
48-
4946
METADATA_FN = 'metadata.json'
5047

5148
USB_TEST_FN = 'usb-test.sd-export'
@@ -66,8 +63,8 @@ class Export:
6663
DISK_ENCRYPTION_KEY_NAME = 'encryption_key'
6764
DISK_EXPORT_DIR = 'export_data'
6865

69-
def __init__(self, is_qubes: bool) -> None:
70-
self.is_qubes = is_qubes
66+
def __init__(self) -> None:
67+
pass
7168

7269
def _export_archive(cls, archive_path: str) -> str:
7370
'''
@@ -92,7 +89,12 @@ def _export_archive(cls, archive_path: str) -> str:
9289
# Python's implementation of subprocess, see
9390
# https://docs.python.org/3/library/subprocess.html#security-considerations
9491
output = subprocess.check_output(
95-
[quote(cls.QVM_OP), quote(cls.QVM_VM), quote(archive_path)],
92+
[
93+
quote('qvm-open-in-vm'),
94+
quote('sd-export-usb'),
95+
quote(archive_path),
96+
'--view-only'
97+
],
9698
stderr=subprocess.STDOUT)
9799
return output.decode('utf-8').strip()
98100
except subprocess.CalledProcessError as e:
@@ -165,8 +167,6 @@ def _run_usb_test(self, archive_dir: str) -> None:
165167
Raises:
166168
ExportError: Raised if the usb-test does not return a USB_CONNECTED status.
167169
'''
168-
logger.debug('Running usb-test...')
169-
170170
archive_path = self._create_archive(archive_dir, self.USB_TEST_FN, self.USB_TEST_METADATA)
171171
status = self._export_archive(archive_path)
172172
if status != ExportStatus.USB_CONNECTED.value:
@@ -182,8 +182,6 @@ def _run_disk_test(self, archive_dir: str) -> None:
182182
Raises:
183183
ExportError: Raised if the usb-test does not return a DISK_ENCRYPTED status.
184184
'''
185-
logger.debug('Running disk-test...')
186-
187185
archive_path = self._create_archive(archive_dir, self.DISK_TEST_FN, self.DISK_TEST_METADATA)
188186

189187
status = self._export_archive(archive_path)
@@ -200,8 +198,6 @@ def _run_disk_export(self, archive_dir: str, filepaths: List[str], passphrase: s
200198
Raises:
201199
ExportError: Raised if the usb-test does not return a DISK_ENCRYPTED status.
202200
'''
203-
logger.debug('Exporting to disk...')
204-
205201
metadata = self.DISK_METADATA.copy()
206202
metadata[self.DISK_ENCRYPTION_KEY_NAME] = passphrase
207203
archive_path = self._create_archive(archive_dir, self.DISK_FN, metadata, filepaths)
@@ -210,15 +206,10 @@ def _run_disk_export(self, archive_dir: str, filepaths: List[str], passphrase: s
210206
if status:
211207
raise ExportError(status)
212208

213-
logger.debug('Export successful')
214-
215209
def run_preflight_checks(self) -> None:
216210
'''
217211
Run preflight checks to verify that the usb device is connected and luks-encrypted.
218212
'''
219-
if not self.is_qubes:
220-
return
221-
222213
with TemporaryDirectory() as temp_dir:
223214
self._run_usb_test(temp_dir)
224215
self._run_disk_test(temp_dir)
@@ -231,8 +222,5 @@ def send_file_to_usb_device(self, filepaths: List[str], passphrase: str) -> None
231222
filepath: The path of file to export.
232223
passphrase: The passphrase to unlock the luks-encrypted usb disk drive.
233224
'''
234-
if not self.is_qubes:
235-
return
236-
237225
with TemporaryDirectory() as temp_dir:
238226
self._run_disk_export(temp_dir, filepaths, passphrase)

securedrop_client/gui/widgets.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1883,7 +1883,7 @@ def __init__(self, controller, file_uuid):
18831883
usb_form_layout = QVBoxLayout()
18841884
self.insert_usb_form.setLayout(usb_form_layout)
18851885
self.usb_error_message = SecureQLabel(_(
1886-
'Either the drive is not encrypted with VeraCrypt, or there is something else wrong'
1886+
'Either the drive is not luks-encrypted, or there is something else wrong'
18871887
'with it. Please try another drive, or see your administrator for help.'))
18881888
self.usb_error_message.setWordWrap(True)
18891889
usb_instructions = SecureQLabel(_(
@@ -1928,11 +1928,18 @@ def __init__(self, controller, file_uuid):
19281928
passphrase_form_layout.addWidget(buttons, alignment=Qt.AlignRight)
19291929
self.passphrase_error_message.hide()
19301930

1931+
# Starting export message
1932+
self.exporting_message = SecureQLabel(_('Exporting...'))
1933+
self.exporting_message.setWordWrap(True)
1934+
19311935
layout.addWidget(self.starting_export_message)
1936+
layout.addWidget(self.exporting_message)
19321937
layout.addWidget(self.generic_error)
19331938
layout.addWidget(self.insert_usb_form)
19341939
layout.addWidget(self.passphrase_form)
19351940

1941+
self.starting_export_message.show()
1942+
self.exporting_message.hide()
19361943
self.generic_error.hide()
19371944
self.insert_usb_form.hide()
19381945
self.passphrase_form.hide()
@@ -1972,6 +1979,9 @@ def _on_retry_export_button_clicked(self):
19721979
@pyqtSlot()
19731980
def _on_unlock_disk_clicked(self):
19741981
try:
1982+
self.passphrase_form.hide()
1983+
self.exporting_message.show()
1984+
QApplication.processEvents()
19751985
passphrase = self.passphrase_field.text()
19761986
self.controller.export_file_to_usb_drive(self.file_uuid, passphrase)
19771987
self.close()
@@ -1990,6 +2000,7 @@ def _request_to_insert_usb_device(self, encryption_not_supported: bool = False):
19902000

19912001
def _request_passphrase(self, bad_passphrase: bool = False):
19922002
self.starting_export_message.hide()
2003+
self.exporting_message.hide()
19932004
self.passphrase_form.show()
19942005
self.insert_usb_form.hide()
19952006

securedrop_client/logic.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def __init__(self, hostname: str, gui, session_maker: sessionmaker,
179179

180180
self.gpg = GpgHelper(home, self.session_maker, proxy)
181181

182-
self.export = Export(self.qubes)
182+
self.export = Export()
183183

184184
self.sync_flag = os.path.join(home, 'sync_flag')
185185

@@ -593,13 +593,29 @@ def on_file_open(self, file_uuid: str) -> None:
593593
logger.info('Opening file "{}".'.format(original_filepath))
594594

595595
def run_export_preflight_checks(self):
596+
'''
597+
Run preflight checks to make sure the Export VM is configured correctly and
598+
'''
599+
logger.debug('Running export preflight checks')
600+
601+
if not self.qubes:
602+
return
603+
596604
self.export.run_preflight_checks()
597605

598606
def export_file_to_usb_drive(self, file_uuid: str, passphrase: str) -> None:
599607
file = self.get_file(file_uuid)
608+
609+
logger.debug('Exporting {}'.format(file.original_filename))
610+
611+
if not self.qubes:
612+
return
613+
600614
filepath = os.path.join(self.data_dir, file.original_filename)
601615
self.export.send_file_to_usb_device([filepath], passphrase)
602616

617+
logger.debug('Export successful')
618+
603619
def on_submission_download(
604620
self,
605621
submission_type: Union[Type[db.File], Type[db.Message]],

tests/test_export.py

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,14 @@ def test_send_file_to_usb_device(mocker):
1414
mock_temp_dir = mocker.MagicMock()
1515
mock_temp_dir.__enter__ = mocker.MagicMock(return_value='mock_temp_dir')
1616
mocker.patch('securedrop_client.export.TemporaryDirectory', return_value=mock_temp_dir)
17-
export = Export(is_qubes=True)
17+
export = Export()
1818
_run_disk_export = mocker.patch.object(export, '_run_disk_export')
1919

2020
export.send_file_to_usb_device(['mock_filepath'], 'mock passphrase')
2121

2222
_run_disk_export.assert_called_once_with('mock_temp_dir', ['mock_filepath'], 'mock passphrase')
2323

2424

25-
def test_send_file_to_usb_device_not_qubes(mocker):
26-
'''
27-
Ensure method returns without error and does not call qubes-only methods.
28-
'''
29-
export = Export(is_qubes=False)
30-
_run_disk_export = mocker.patch.object(export, '_run_disk_export')
31-
32-
export.send_file_to_usb_device(['mock_filepath'], 'mock passphrase')
33-
34-
_run_disk_export.assert_not_called()
35-
36-
3725
def test_run_preflight_checks(mocker):
3826
'''
3927
Ensure TemporaryDirectory is used when creating and sending the archives during the preflight
@@ -42,7 +30,7 @@ def test_run_preflight_checks(mocker):
4230
mock_temp_dir = mocker.MagicMock()
4331
mock_temp_dir.__enter__ = mocker.MagicMock(return_value='mock_temp_dir')
4432
mocker.patch('securedrop_client.export.TemporaryDirectory', return_value=mock_temp_dir)
45-
export = Export(is_qubes=True)
33+
export = Export()
4634
_run_usb_export = mocker.patch.object(export, '_run_usb_test')
4735
_run_disk_export = mocker.patch.object(export, '_run_disk_test')
4836

@@ -52,27 +40,13 @@ def test_run_preflight_checks(mocker):
5240
_run_disk_export.assert_called_once_with('mock_temp_dir')
5341

5442

55-
def test_run_preflight_checks_not_qubes(mocker):
56-
'''
57-
Ensure method returns without error and does not call qubes-only methods.
58-
'''
59-
export = Export(is_qubes=False)
60-
_run_usb_test = mocker.patch.object(export, '_run_usb_test')
61-
_run_disk_test = mocker.patch.object(export, '_run_disk_test')
62-
63-
export.run_preflight_checks()
64-
65-
_run_usb_test.assert_not_called()
66-
_run_disk_test.assert_not_called()
67-
68-
6943
def test__run_disk_export(mocker):
7044
'''
7145
Ensure _export_archive and _create_archive are called with the expected parameters,
7246
_export_archive is called with the return value of _create_archive, and
7347
_run_disk_test returns without error if '' is the ouput status of _export_archive.
7448
'''
75-
export = Export(is_qubes=False)
49+
export = Export()
7650
export._create_archive = mocker.MagicMock(return_value='mock_archive_path')
7751
export._export_archive = mocker.MagicMock(return_value='')
7852

@@ -94,7 +68,7 @@ def test__run_disk_export_raises_ExportError_if_not_empty_string(mocker):
9468
'''
9569
Ensure ExportError is raised if _run_disk_test returns anything other than ''.
9670
'''
97-
export = Export(is_qubes=True)
71+
export = Export()
9872
export._create_archive = mocker.MagicMock(return_value='mock_archive_path')
9973
export._export_archive = mocker.MagicMock(return_value='SOMETHING_OTHER_THAN_EMPTY_STRING')
10074

@@ -108,7 +82,7 @@ def test__run_disk_test(mocker):
10882
_export_archive is called with the return value of _create_archive, and
10983
_run_disk_test returns without error if 'USB_ENCRYPTED' is the ouput status of _export_archive.
11084
'''
111-
export = Export(is_qubes=False)
85+
export = Export()
11286
export._create_archive = mocker.MagicMock(return_value='mock_archive_path')
11387
export._export_archive = mocker.MagicMock(return_value='USB_ENCRYPTED')
11488

@@ -123,7 +97,7 @@ def test__run_disk_test_raises_ExportError_if_not_USB_ENCRYPTED(mocker):
12397
'''
12498
Ensure ExportError is raised if _run_disk_test returns anything other than 'USB_ENCRYPTED'.
12599
'''
126-
export = Export(is_qubes=True)
100+
export = Export()
127101
export._create_archive = mocker.MagicMock(return_value='mock_archive_path')
128102
export._export_archive = mocker.MagicMock(return_value='SOMETHING_OTHER_THAN_USB_ENCRYPTED')
129103

@@ -137,7 +111,7 @@ def test__run_usb_test(mocker):
137111
_export_archive is called with the return value of _create_archive, and
138112
_run_disk_test returns without error if 'USB_CONNECTED' is the return value of _export_archive.
139113
'''
140-
export = Export(is_qubes=False)
114+
export = Export()
141115
export._create_archive = mocker.MagicMock(return_value='mock_archive_path')
142116
export._export_archive = mocker.MagicMock(return_value='USB_CONNECTED')
143117

@@ -152,7 +126,7 @@ def test__run_usb_test_raises_ExportError_if_not_USB_CONNECTED(mocker):
152126
'''
153127
Ensure ExportError is raised if _run_disk_test returns anything other than 'USB_CONNECTED'.
154128
'''
155-
export = Export(is_qubes=True)
129+
export = Export()
156130
export._create_archive = mocker.MagicMock(return_value='mock_archive_path')
157131
export._export_archive = mocker.MagicMock(return_value='SOMETHING_OTHER_THAN_USB_CONNECTED')
158132

@@ -164,7 +138,7 @@ def test__create_archive(mocker):
164138
'''
165139
Ensure _create_archive creates an archive in the supplied directory.
166140
'''
167-
export = Export(is_qubes=True)
141+
export = Export()
168142
archive_path = None
169143
with TemporaryDirectory() as temp_dir:
170144
archive_path = export._create_archive(temp_dir, 'mock.sd-export', {})
@@ -175,7 +149,7 @@ def test__create_archive(mocker):
175149

176150

177151
def test__create_archive_with_an_export_file(mocker):
178-
export = Export(is_qubes=True)
152+
export = Export()
179153
archive_path = None
180154
with TemporaryDirectory() as temp_dir, NamedTemporaryFile() as export_file:
181155
archive_path = export._create_archive(temp_dir, 'mock.sd-export', {}, [export_file.name])
@@ -189,7 +163,7 @@ def test__create_archive_with_multiple_export_files(mocker):
189163
'''
190164
Ensure an archive
191165
'''
192-
export = Export(is_qubes=True)
166+
export = Export()
193167
archive_path = None
194168
with TemporaryDirectory() as temp_dir, \
195169
NamedTemporaryFile() as export_file_one, \
@@ -207,7 +181,7 @@ def test__export_archive(mocker):
207181
Ensure the subprocess call returns the expected output.
208182
'''
209183
mocker.patch('subprocess.check_output', return_value=b'mock')
210-
export = Export(is_qubes=True)
184+
export = Export()
211185
status = export._export_archive('mock.sd-export')
212186

213187
assert status == 'mock'
@@ -220,7 +194,7 @@ def test__export_archive_does_not_raise_ExportError_when_CalledProcessError(mock
220194
mock_error = subprocess.CalledProcessError('mock_cmd', 123)
221195
mocker.patch('subprocess.check_output', side_effect=mock_error)
222196

223-
export = Export(is_qubes=True)
197+
export = Export()
224198

225199
with pytest.raises(ExportError, match='CALLED_PROCESS_ERROR'):
226200
export._export_archive('mock.sd-export')
@@ -230,10 +204,10 @@ def test__export_archive_with_evil_command(mocker):
230204
'''
231205
Ensure shell command is shell-escaped.
232206
'''
233-
export = Export(is_qubes=False)
207+
export = Export()
234208
check_output = mocker.patch('subprocess.check_output', return_value=b'')
235209

236210
export._export_archive('somefile; rm -rf ~')
237211

238212
check_output.assert_called_once_with(
239-
['qvm-open-in-vm', 'sd-export-usb', "'somefile; rm -rf ~'"], stderr=-2)
213+
['qvm-open-in-vm', 'sd-export-usb', "'somefile; rm -rf ~'", '--view-only'], stderr=-2)

tests/test_logic.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1417,16 +1417,30 @@ def test_Controller_call_update_star_success(homedir, config, mocker, session_ma
14171417

14181418

14191419
def test_Controller_run_export_preflight_checks(homedir, mocker):
1420+
debug_logger = mocker.patch('securedrop_client.logic.logger.debug')
14201421
co = Controller('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir)
14211422
co.export = mocker.MagicMock()
14221423

14231424
co.run_export_preflight_checks()
14241425

14251426
co.export.run_preflight_checks.assert_called_once_with()
1427+
debug_logger.assert_called_once_with('Running export preflight checks')
14261428

14271429

1428-
def test_Controller_export_file_to_usb_drive(homedir, mocker, session):
1430+
def test_Controller_run_export_preflight_checks_not_qubes(homedir, mocker):
1431+
debug_logger = mocker.patch('securedrop_client.logic.logger.debug')
1432+
co = Controller('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir)
1433+
co.qubes = False
1434+
co.export = mocker.MagicMock()
1435+
1436+
co.run_export_preflight_checks()
1437+
1438+
co.export.run_preflight_checks.assert_not_called()
1439+
debug_logger.assert_called_once_with('Running export preflight checks')
1440+
14291441

1442+
def test_Controller_export_file_to_usb_drive(homedir, mocker, session):
1443+
debug_logger = mocker.patch('securedrop_client.logic.logger.debug')
14301444
co = Controller('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir)
14311445
co.export = mocker.MagicMock()
14321446
file = factory.File(source=factory.Source(), original_filename='mock_filename')
@@ -1438,3 +1452,21 @@ def test_Controller_export_file_to_usb_drive(homedir, mocker, session):
14381452

14391453
co.export.send_file_to_usb_device.assert_called_once_with(
14401454
[os.path.join(co.data_dir, 'mock_filename')], 'mock passphrase')
1455+
debug_logger.call_args_list[0][0][0] == 'Exporting {}'.format(file.original_filename)
1456+
debug_logger.call_args_list[1][0][0] == 'Export successful'
1457+
1458+
1459+
def test_Controller_export_file_to_usb_drive_not_qubes(homedir, mocker, session):
1460+
debug_logger = mocker.patch('securedrop_client.logic.logger.debug')
1461+
co = Controller('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir)
1462+
co.qubes = False
1463+
co.export = mocker.MagicMock()
1464+
file = factory.File(source=factory.Source(), original_filename='mock_filename')
1465+
session.add(file)
1466+
session.commit()
1467+
mocker.patch('securedrop_client.logic.Controller.get_file', return_value=file)
1468+
1469+
co.export_file_to_usb_drive(file.uuid, 'mock passphrase')
1470+
1471+
co.export.send_file_to_usb_device.assert_not_called()
1472+
debug_logger.call_args_list[0][0][0] == 'Exporting {}'.format(file.original_filename)

0 commit comments

Comments
 (0)