-
Notifications
You must be signed in to change notification settings - Fork 42
/
file_dialog.py
288 lines (256 loc) Β· 12.3 KB
/
file_dialog.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
"""
A dialog that allows journalists to export sensitive files to a USB drive.
"""
from gettext import gettext as _
from typing import Optional
from pkg_resources import resource_string
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QGraphicsDropShadowEffect, QLineEdit, QVBoxLayout, QWidget
from securedrop_client.export import ExportError
from securedrop_client.export_status import ExportStatus
from securedrop_client.gui.base import ModalDialog, PasswordEdit, SecureQLabel
from securedrop_client.gui.base.checkbox import SDCheckBox
from .device import Device
class FileDialog(ModalDialog):
DIALOG_CSS = resource_string(__name__, "dialog.css").decode("utf-8")
PASSPHRASE_LABEL_SPACING = 0.5
NO_MARGIN = 0
FILENAME_WIDTH_PX = 260
def __init__(self, device: Device, file_uuid: str, file_name: str) -> None:
super().__init__()
self.setStyleSheet(self.DIALOG_CSS)
self._device = device
self.file_uuid = file_uuid
self.file_name = SecureQLabel(
file_name, wordwrap=False, max_length=self.FILENAME_WIDTH_PX
).text()
# Hold onto the error status we receive from the Export VM
self.error_status: Optional[ExportStatus] = None
# Connect device signals to slots
self._device.export_preflight_check_succeeded.connect(
self._on_export_preflight_check_succeeded
)
self._device.export_preflight_check_failed.connect(self._on_export_preflight_check_failed)
self._device.export_succeeded.connect(self._on_export_succeeded)
self._device.export_failed.connect(self._on_export_failed)
# Connect parent signals to slots
self.continue_button.setEnabled(False)
self.continue_button.clicked.connect(self._run_preflight)
# Dialog content
self.starting_header = _(
"Preparing to export:<br />" '<span style="font-weight:normal">{}</span>'
).format(self.file_name)
self.ready_header = _(
"Ready to export:<br />" '<span style="font-weight:normal">{}</span>'
).format(self.file_name)
self.insert_usb_header = _("Insert encrypted USB drive")
self.passphrase_header = _("Enter passphrase for USB drive")
self.success_header = _("Export successful")
self.error_header = _("Export failed")
self.starting_message = _(
"<h2>Understand the risks before exporting files</h2>"
"<b>Malware</b>"
"<br />"
"This workstation lets you open files securely. If you open files on another "
"computer, any embedded malware may spread to your computer or network. If you are "
"unsure how to manage this risk, please print the file, or contact your "
"administrator."
"<br /><br />"
"<b>Anonymity</b>"
"<br />"
"Files submitted by sources may contain information or hidden metadata that "
"identifies who they are. To protect your sources, please consider redacting files "
"before working with them on network-connected computers."
)
self.exporting_message = _("Exporting: {}").format(self.file_name)
self.insert_usb_message = _(
"Please insert one of the export drives provisioned specifically "
"for the SecureDrop Workstation."
)
self.usb_error_message = _(
"Either the drive is not encrypted or there is something else wrong with it."
)
self.passphrase_error_message = _("The passphrase provided did not work. Please try again.")
self.generic_error_message = _("See your administrator for help.")
self.success_message = _(
"Remember to be careful when working with files outside of your Workstation machine."
)
# Passphrase Form
self.passphrase_form = QWidget()
self.passphrase_form.setObjectName("FileDialog_passphrase_form")
passphrase_form_layout = QVBoxLayout()
passphrase_form_layout.setContentsMargins(
self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN
)
self.passphrase_form.setLayout(passphrase_form_layout)
passphrase_label = SecureQLabel(_("Passphrase"))
font = QFont()
font.setLetterSpacing(QFont.AbsoluteSpacing, self.PASSPHRASE_LABEL_SPACING)
passphrase_label.setFont(font)
self.passphrase_field = PasswordEdit(self)
self.passphrase_field.setEchoMode(QLineEdit.Password)
effect = QGraphicsDropShadowEffect(self)
effect.setOffset(0, -1)
effect.setBlurRadius(4)
effect.setColor(QColor("#aaa"))
self.passphrase_field.setGraphicsEffect(effect)
check = SDCheckBox()
check.checkbox.stateChanged.connect(self.passphrase_field.on_toggle_password_Action)
passphrase_form_layout.addWidget(passphrase_label)
passphrase_form_layout.addWidget(self.passphrase_field)
passphrase_form_layout.addWidget(check, alignment=Qt.AlignRight)
self.body_layout.addWidget(self.passphrase_form)
self.passphrase_form.hide()
self._show_starting_instructions()
self.start_animate_header()
self._run_preflight()
def _show_starting_instructions(self) -> None:
self.header.setText(self.starting_header)
self.body.setText(self.starting_message)
self.adjustSize()
def _show_passphrase_request_message(self) -> None:
self.continue_button.clicked.disconnect()
self.continue_button.clicked.connect(self._export_file)
self.header.setText(self.passphrase_header)
self.continue_button.setText(_("SUBMIT"))
self.header_line.hide()
self.error_details.hide()
self.body.hide()
self.passphrase_field.setFocus()
self.passphrase_form.show()
self.adjustSize()
def _show_passphrase_request_message_again(self) -> None:
self.continue_button.clicked.disconnect()
self.continue_button.clicked.connect(self._export_file)
self.header.setText(self.passphrase_header)
self.error_details.setText(self.passphrase_error_message)
self.continue_button.setText(_("SUBMIT"))
self.header_line.hide()
self.body.hide()
self.error_details.show()
self.passphrase_field.setFocus()
self.passphrase_form.show()
self.adjustSize()
def _show_success_message(self) -> None:
self.continue_button.clicked.disconnect()
self.continue_button.clicked.connect(self.close)
self.header.setText(self.success_header)
self.continue_button.setText(_("DONE"))
self.body.setText(self.success_message)
self.cancel_button.hide()
self.error_details.hide()
self.passphrase_form.hide()
self.header_line.show()
self.body.show()
self.adjustSize()
def _show_insert_usb_message(self) -> None:
self.continue_button.clicked.disconnect()
self.continue_button.clicked.connect(self._run_preflight)
self.header.setText(self.insert_usb_header)
self.continue_button.setText(_("CONTINUE"))
self.body.setText(self.insert_usb_message)
self.error_details.hide()
self.passphrase_form.hide()
self.header_line.show()
self.body.show()
self.adjustSize()
def _show_insert_encrypted_usb_message(self) -> None:
self.continue_button.clicked.disconnect()
self.continue_button.clicked.connect(self._run_preflight)
self.header.setText(self.insert_usb_header)
self.error_details.setText(self.usb_error_message)
self.continue_button.setText(_("CONTINUE"))
self.body.setText(self.insert_usb_message)
self.passphrase_form.hide()
self.header_line.show()
self.error_details.show()
self.body.show()
self.adjustSize()
def _show_generic_error_message(self) -> None:
self.continue_button.clicked.disconnect()
self.continue_button.clicked.connect(self.close)
self.continue_button.setText(_("DONE"))
self.header.setText(self.error_header)
self.body.setText( # nosemgrep: semgrep.untranslated-gui-string
"{}: {}".format(self.error_status, self.generic_error_message)
)
self.error_details.hide()
self.passphrase_form.hide()
self.header_line.show()
self.body.show()
self.adjustSize()
@pyqtSlot()
def _run_preflight(self) -> None:
self._device.run_export_preflight_checks()
@pyqtSlot()
def _export_file(self, checked: bool = False) -> None:
self.start_animate_activestate()
self.cancel_button.setEnabled(False)
self.passphrase_field.setDisabled(True)
# TODO: If the drive is already unlocked, the passphrase field will be empty.
# This is ok, but could violate expectations. The password should be passed
# via qrexec in future, to avoid writing it to even a temporary file at all.
self._device.export_file_to_usb_drive(self.file_uuid, self.passphrase_field.text())
@pyqtSlot(object)
def _on_export_preflight_check_succeeded(self, result: ExportStatus) -> None:
# If the continue button is disabled then this is the result of a background preflight check
self.stop_animate_header()
self.header_icon.update_image("savetodisk.svg", QSize(64, 64))
self.header.setText(self.ready_header)
if not self.continue_button.isEnabled():
self.continue_button.clicked.disconnect()
if result == ExportStatus.DEVICE_WRITABLE:
# Skip password prompt, we're there
self.continue_button.clicked.connect(self._export_file)
else: # result == ExportStatus.DEVICE_LOCKED
self.continue_button.clicked.connect(self._show_passphrase_request_message)
self.continue_button.setEnabled(True)
self.continue_button.setFocus()
return
# Skip passphrase prompt if device is unlocked
if result == ExportStatus.DEVICE_WRITABLE:
self._export_file()
else:
self._show_passphrase_request_message()
@pyqtSlot(object)
def _on_export_preflight_check_failed(self, error: ExportError) -> None:
self.stop_animate_header()
self.header_icon.update_image("savetodisk.svg", QSize(64, 64))
self._update_dialog(error.status)
@pyqtSlot(object)
def _on_export_succeeded(self, status: ExportStatus) -> None:
self.stop_animate_activestate()
self._show_success_message()
@pyqtSlot(object)
def _on_export_failed(self, error: ExportError) -> None:
self.stop_animate_activestate()
self.cancel_button.setEnabled(True)
self.passphrase_field.setDisabled(False)
self._update_dialog(error.status)
def _update_dialog(self, error_status: ExportStatus) -> None:
self.error_status = error_status
# If the continue button is disabled then this is the result of a background preflight check
if not self.continue_button.isEnabled():
self.continue_button.clicked.disconnect()
if self.error_status == ExportStatus.ERROR_UNLOCK_LUKS:
self.continue_button.clicked.connect(self._show_passphrase_request_message_again)
elif self.error_status == ExportStatus.NO_DEVICE_DETECTED: # fka USB_NOT_CONNECTED
self.continue_button.clicked.connect(self._show_insert_usb_message)
elif (
self.error_status == ExportStatus.INVALID_DEVICE_DETECTED
): # fka DISK_ENCRYPTION_NOT_SUPPORTED_ERROR
self.continue_button.clicked.connect(self._show_insert_encrypted_usb_message)
else:
self.continue_button.clicked.connect(self._show_generic_error_message)
self.continue_button.setEnabled(True)
self.continue_button.setFocus()
else:
if self.error_status == ExportStatus.ERROR_UNLOCK_LUKS:
self._show_passphrase_request_message_again()
elif self.error_status == ExportStatus.NO_DEVICE_DETECTED:
self._show_insert_usb_message()
elif self.error_status == ExportStatus.INVALID_DEVICE_DETECTED:
self._show_insert_encrypted_usb_message()
else:
self._show_generic_error_message()