From 81552df69523c752775a7e123f955ea30172e049 Mon Sep 17 00:00:00 2001 From: mickael e Date: Mon, 22 Jul 2019 12:41:53 -0400 Subject: [PATCH] Remove salt provisioning for sd-export files These tasks should now be done via the securedrop-export package: https://github.com/freedomofpress/securedrop-export Install securedrop-export package in sd-export-template --- dom0/sd-export-files.sls | 60 +---- sd-export/application-x-sd-export.xml | 7 - sd-export/send-to-usb | 356 -------------------------- sd-export/send-to-usb.desktop | 5 - sd-export/test_export.py | 158 ------------ tests/test_sd_export.py | 1 + 6 files changed, 5 insertions(+), 582 deletions(-) delete mode 100644 sd-export/application-x-sd-export.xml delete mode 100755 sd-export/send-to-usb delete mode 100644 sd-export/send-to-usb.desktop delete mode 100644 sd-export/test_export.py diff --git a/dom0/sd-export-files.sls b/dom0/sd-export-files.sls index 8d4626cc8..aa0bc9dfb 100644 --- a/dom0/sd-export-files.sls +++ b/dom0/sd-export-files.sls @@ -11,19 +11,6 @@ include: - fpf-apt-test-repo -sd-export-template-install-packages: - pkg.installed: - - pkgs: - - cryptsetup - - cups - - task-print-server - - system-config-printer - - xpp - - libcups2-dev - - python3-dev - - libtool-bin - - unoconv - # Libreoffice needs to be installed here to convert to pdf to allow printing sd-export-install-libreoffice: pkg.installed: @@ -33,47 +20,8 @@ sd-export-install-libreoffice: interval: 60 - install_recommends: False -sd-export-send-to-usb-script: - file.managed: - - name: /usr/bin/send-to-usb - - source: salt://sd/sd-export/send-to-usb - - user: root - - group: root - - mode: 755 - - makedirs: True - -sd-export-desktop-file: - file.managed: - - name: /usr/share/applications/send-to-usb.desktop - - source: salt://sd/sd-export/send-to-usb.desktop - - user: root - - group: root - - mode: 644 - - makedirs: True - cmd.run: - - name: sudo update-desktop-database /usr/share/applications - - require: - - file: sd-export-desktop-file - -sd-export-file-format: - file.managed: - - name: /usr/share/mime/packages/application-x-sd-export.xml - - source: salt://sd/sd-export/application-x-sd-export.xml - - user: root - - group: root - - mode: 644 - - makedirs: True - cmd.run: - - name: sudo update-mime-database /usr/share/mime - - require: - - file: sd-export-file-format - - file: sd-export-desktop-file +# Install securedrop-export package https://github.com/freedomofpress/securedrop-export +sd-export-install-package: + pkg.installed: + - name: securedrop-export -sd-export-securedrop-icon: - file.managed: - - name: /usr/share/securedrop/icons/sd-logo.png - - source: salt://sd/sd-proxy/logo-small.png - - user: root - - group: root - - mode: 644 - - makedirs: True diff --git a/sd-export/application-x-sd-export.xml b/sd-export/application-x-sd-export.xml deleted file mode 100644 index 9e36ef08b..000000000 --- a/sd-export/application-x-sd-export.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - Archive for transfering files from the SecureDrop workstation to an external USB device. - - - diff --git a/sd-export/send-to-usb b/sd-export/send-to-usb deleted file mode 100755 index 1f5969f84..000000000 --- a/sd-export/send-to-usb +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import json -import os -import shutil -import signal -import subprocess -import sys -import tarfile -import tempfile -import time - - -def exit_gracefully(msg, e=False): - """ - Utility to print error messages, mostly used during debugging, - then exits successfully despite the error. Always exits 0, - since non-zero exit values will cause system to try alternative - solutions for mimetype handling, which we want to avoid. - """ - sys.stderr.write(msg) - sys.stderr.write("\n") - if e: - try: - # If the file archive was extracted, delete before returning - if os.path.isdir(SUBMISSION_TMPDIR): - shutil.rmtree(SUBMISSION_TMPDIR) - e_output = e.output - except Exception: - e_output = "" - sys.stderr.write(e_output) - sys.stderr.write("\n") - # exit with 0 return code otherwise the os will attempt to open - # the file with another application - popup_message("Export error: {}".format(msg)) - sys.exit(0) - - -def popup_message(msg): - try: - subprocess.check_call([ - "notify-send", - "--expire-time", "3000", - "--icon", "/usr/share/securedrop/icons/sd-logo.png", - "SecureDrop: {}".format(msg) - ]) - except subprocess.CalledProcessError as e: - msg = "Error sending notification:" - exit_gracefully(msg, e=e) - - -class Metadata(object): - """ - Object to parse, validate and store json metadata from the sd-export archive. - """ - - METADATA_FILE = "metadata.json" - SUPPORTED_EXPORT_METHODS = ["disk", "printer", "printer-test"] - SUPPORTED_ENCRYPTION_METHODS = ["luks"] - - def __init__(self, archive_path): - self.metadata_path = os.path.join(archive_path, self.METADATA_FILE) - try: - with open(self.metadata_path) as f: - json_config = json.loads(f.read()) - self.export_method = json_config.get("device", None) - self.encryption_method = json_config.get("encryption_method", None) - self.encryption_key = json_config.get("encryption_key", None) - except Exception as e: - msg = "Error parsing metadata: " - exit_gracefully(msg, e=e) - - def is_valid(self): - if self.export_method not in self.SUPPORTED_EXPORT_METHODS: - return False - - if self.export_method == "disk": - if self.encryption_method not in self.SUPPORTED_ENCRYPTION_METHODS: - return False - return True - - -def extract_tarball(): - try: - with tarfile.open(SUBMISSION_ARCHIVE) as tar: - tar.extractall(SUBMISSION_TMPDIR) - except Exception as e: - msg = "Error opening export bundle: " - exit_gracefully(msg, e=e) - - -def unlock_luks_volume(encryption_key): - # the luks device is not already unlocked - if not os.path.exists(os.path.join("/dev/mapper/", ENCRYPTED_DEVICE)): - p = subprocess.Popen( - ["sudo", "cryptsetup", "luksOpen", DEVICE, ENCRYPTED_DEVICE], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - p.communicate(input=str.encode(encryption_key, "utf-8")) - rc = p.returncode - if rc != 0: - msg = "Bad passphrase or luks error." - exit_gracefully(msg) - - -def mount_volume(): - # mount target not created - if not os.path.exists(MOUNTPOINT): - subprocess.check_call(["sudo", "mkdir", MOUNTPOINT]) - try: - subprocess.check_call( - [ - "sudo", - "mount", - os.path.join("/dev/mapper/", ENCRYPTED_DEVICE), - MOUNTPOINT, - ] - ) - subprocess.check_call(["sudo", "chown", "-R", "user:user", MOUNTPOINT]) - except subprocess.CalledProcessError as e: - # clean up - subprocess.check_call(["sudo", "cryptsetup", "luksClose", ENCRYPTED_DEVICE]) - subprocess.check_call(["rm", "-rf", SUBMISSION_TMPDIR]) - msg = "An error occurred while mounting disk: " - exit_gracefully(msg, e=e) - - -def copy_submission(): - # move files to drive (overwrites files with same filename) and unmount drive - try: - TARGET_DIRNAME_path = os.path.join(MOUNTPOINT, TARGET_DIRNAME) - subprocess.check_call(["mkdir", TARGET_DIRNAME_path]) - export_data = os.path.join( - SUBMISSION_TMPDIR, SUBMISSION_DIRNAME, "export_data/" - ) - subprocess.check_call(["cp", "-r", export_data, TARGET_DIRNAME_path]) - popup_message("Files exported successfully to disk.") - except (subprocess.CalledProcessError, OSError) as e: - msg = "Error writing to disk:" - exit_gracefully(msg, e=e) - finally: - # Finally, we sync the filesystem, unmount the drive and lock the - # luks volume, and exit 0 - subprocess.check_call(["sync"]) - subprocess.check_call(["sudo", "umount", MOUNTPOINT]) - subprocess.check_call(["sudo", "cryptsetup", "luksClose", ENCRYPTED_DEVICE]) - subprocess.check_call(["rm", "-rf", SUBMISSION_TMPDIR]) - sys.exit(0) - - -class TimeoutException(Exception): - pass - - -def handler(s, f): - raise TimeoutException("Timeout") - - -def wait_for_print(): - # use lpstat to ensure the job was fully transfered to the printer - # returns True if print was successful, otherwise will throw exceptions - signal.signal(signal.SIGALRM, handler) - signal.alarm(PRINTER_WAIT_TIMEOUT) - printer_idle_string = "printer {} is idle".format(PRINTER_NAME) - while(True): - try: - output = subprocess.check_output(["lpstat", "-p", PRINTER_NAME]) - if(printer_idle_string in output.decode("utf-8")): - return True - else: - time.sleep(5) - except subprocess.CalledProcessError as e: - msg = "Error while retrieving print status" - exit_gracefully(msg, e=e) - except TimeoutException as e: - msg = "Timeout when getting printer information" - exit_gracefully(msg, e=e) - return True - - -def get_printer_uri(): - # Get the URI via lpinfo and only accept URIs of supported printers - printer_uri = "" - try: - output = subprocess.check_output(["sudo", "lpinfo", "-v"]) - except subprocess.CalledProcessError as e: - msg = "Error retrieving printer uri." - exit_gracefully(msg, e=e) - - # fetch the usb printer uri - for line in output.split(): - if "usb://" in line.decode("utf-8"): - printer_uri = line.decode("utf-8") - - # verify that the printer is supported, else exit - if printer_uri == "": - # No usb printer is connected - exit_gracefully("USB Printer not found") - elif "Brother" in printer_uri: - return printer_uri - else: - # printer url is a make that is unsupported - exit_gracefully("USB Printer not supported") - - -def install_printer_ppd(uri): - # Some drivers don't come with ppd files pre-compiled, we must compile them - if "Brother" in uri: - try: - subprocess.check_call( - ["sudo", "ppdc", BRLASER_DRIVER, "-d", "/usr/share/cups/model/"] - ) - except subprocess.CalledProcessError as e: - msg = "Error installing ppd file for printer {}.".format(uri) - exit_gracefully(msg, e=e) - return BRLASER_PPD - # Here, we could support ppd drivers for other makes or models in the future - - -def setup_printer(printer_name, printer_uri, printer_ppd): - try: - # Add the printer using lpadmin - subprocess.check_call( - [ - "sudo", - "lpadmin", - "-p", - printer_name, - "-v", - printer_uri, - "-P", - printer_ppd, - ] - ) - # Activate the printer so that it can receive jobs - subprocess.check_call(["sudo", "lpadmin", "-p", printer_name, "-E"]) - # Allow user to print (without using sudo) - subprocess.check_call( - ["sudo", "lpadmin", "-p", printer_name, "-u", "allow:user"] - ) - except subprocess.CalledProcessError as e: - msg = "Error setting up printer {} at {} using {}.".format( - printer_name, printer_uri, printer_ppd - ) - exit_gracefully(msg, e=e) - - -def print_test_page(printer_name): - print_file(printer_name, "/usr/share/cups/data/testprint") - popup_message("Printing test page") - - -def print_all_files(printer_name): - files_path = os.path.join(SUBMISSION_TMPDIR, SUBMISSION_DIRNAME, "export_data/") - files = os.listdir(files_path) - print_count = 0 - for f in files: - file_path = os.path.join(files_path, f) - print_file(printer_name, file_path) - print_count += 1 - msg = "Printing document {} of {}".format(print_count, len(files)) - popup_message(msg) - - -def is_open_office_file(filename): - OPEN_OFFICE_FORMATS = [".doc", ".docx", ".xls", ".xlsx", - ".ppt", ".pptx", ".odt", ".ods", ".odp"] - for extension in OPEN_OFFICE_FORMATS: - if os.path.basename(filename).endswith(extension): - return True - return False - - -def print_file(printer_name, file_to_print): - try: - # if the file to print is an (open)office document, we need to call unoconf to convert - # the file to pdf as printer drivers do not immediately support this format out of the box - if is_open_office_file(file_to_print): - folder = os.path.dirname(file_to_print) - converted_filename = file_to_print + ".pdf" - converted_path = os.path.join(folder, converted_filename) - subprocess.check_call(["unoconv", "-o", converted_path, file_to_print]) - file_to_print = converted_path - - subprocess.check_call(["xpp", "-P", printer_name, file_to_print]) - except subprocess.CalledProcessError as e: - msg = "Error printing file {} with printer {}.".format( - file_to_print, printer_name - ) - exit_gracefully(msg, e=e) - - -def main(): - extract_tarball() - - archive_path = os.path.join(SUBMISSION_TMPDIR, SUBMISSION_DIRNAME) - archive_metadata = Metadata(archive_path) - if archive_metadata.is_valid(): - if archive_metadata.export_method == "disk": - # exports all documents in the archive to luks-encrypted volume - unlock_luks_volume(archive_metadata.encryption_key) - mount_volume() - copy_submission() - elif archive_metadata.export_method == "printer": - # prints all documents in the archive - printer_uri = get_printer_uri() - printer_ppd = install_printer_ppd(printer_uri) - setup_printer(PRINTER_NAME, printer_uri, printer_ppd) - print_all_files(PRINTER_NAME) - elif archive_metadata.export_method == "printer-test": - # Prints a test page to ensure the printer is functional - printer_uri = get_printer_uri() - printer_ppd = install_printer_ppd(printer_uri) - setup_printer(PRINTER_NAME, printer_uri, printer_ppd) - print_test_page(PRINTER_NAME) - else: - exit_gracefully("Archive metadata is invalid") - - -if __name__ == "__main__": - PRINTER_NAME = "sdw-printer" - try: - # We define globals inside the main block, rather than at the top - # of the file, to catch exceptions via exit_gracefully. All var names - # will be available to all functions called via main(). - DEVICE = "/dev/sda1" - MOUNTPOINT = "/media/usb" - ENCRYPTED_DEVICE = "encrypted_volume" - SUBMISSION_ARCHIVE = sys.argv[1] - BRLASER_DRIVER = "/usr/share/cups/drv/brlaser.drv" - BRLASER_PPD = "/usr/share/cups/model/br7030.ppd" - PRINTER_NAME = "sdw-printer" - PRINTER_WAIT_TIMEOUT = 60 - # Halt immediately if target file is absent - if not os.path.exists(SUBMISSION_ARCHIVE): - msg = "File does not exist" - exit_gracefully(msg) - - SUBMISSION_DIRNAME = os.path.basename(SUBMISSION_ARCHIVE).split(".")[0] - TARGET_DIRNAME = "sd-export-{}".format( - datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - ) - SUBMISSION_TMPDIR = tempfile.mkdtemp() - - main() - - # Delete extracted achive from tempfile - shutil.rmtree(SUBMISSION_TMPDIR) - except Exception as e: - # exit with 0 return code otherwise the os will attempt to open - # the file with another application - msg = "Unhandled exception:" - exit_gracefully(msg, e=e) diff --git a/sd-export/send-to-usb.desktop b/sd-export/send-to-usb.desktop deleted file mode 100644 index 6521a92c9..000000000 --- a/sd-export/send-to-usb.desktop +++ /dev/null @@ -1,5 +0,0 @@ -[Desktop Entry] -Type=Application -MimeType=application/x-sd-export -Name="Export SD submission to USB" -Exec=/usr/bin/send-to-usb diff --git a/sd-export/test_export.py b/sd-export/test_export.py deleted file mode 100644 index 52af54678..000000000 --- a/sd-export/test_export.py +++ /dev/null @@ -1,158 +0,0 @@ -from unittest import mock - -import imp -import os -import pytest -import tempfile - - -SAMPLE_OUTPUT_NO_PRINTER = b"network beh\nnetwork https\nnetwork ipp\nnetwork ipps\nnetwork http\nnetwork\nnetwork ipp14\nnetwork lpd" # noqa -SAMPLE_OUTPUT_BOTHER_PRINTER = b"network beh\nnetwork https\nnetwork ipp\nnetwork ipps\nnetwork http\nnetwork\nnetwork ipp14\ndirect usb://Brother/HL-L2320D%20series?serial=A00000A000000\nnetwork lpd" # noqa - - -# This below stanza is only necessary because the export code is not -# structured as a module. If a Python module were created called -# `securedropexport`, we could simply do `import securedropexport` -path_to_script = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "send-to-usb" -) -securedropexport = imp.load_source("send-to-usb", path_to_script) - - -def test_exit_gracefully_no_exception(capsys): - test_msg = 'test' - - with pytest.raises(SystemExit) as sysexit: - securedropexport.exit_gracefully(test_msg) - - # A graceful exit means a return code of 0 - assert sysexit.value.code == 0 - - captured = capsys.readouterr() - assert captured.err == "{}\n".format(test_msg) - assert captured.out == "" - - -def test_exit_gracefully_exception(capsys): - test_msg = 'test' - - with pytest.raises(SystemExit) as sysexit: - securedropexport.exit_gracefully(test_msg, - e=Exception('BANG!')) - - # A graceful exit means a return code of 0 - assert sysexit.value.code == 0 - - captured = capsys.readouterr() - assert captured.err == "{}\n\n".format(test_msg) - assert captured.out == "" - - -def test_empty_config(capsys): - temp_folder = tempfile.mkdtemp() - metadata = os.path.join(temp_folder, securedropexport.Metadata.METADATA_FILE) - with open(metadata, "w") as f: - f.write("{}") - config = securedropexport.Metadata(temp_folder) - assert not config.is_valid() - - -def test_valid_printer_test_config(capsys): - temp_folder = tempfile.mkdtemp() - metadata = os.path.join(temp_folder, securedropexport.Metadata.METADATA_FILE) - with open(metadata, "w") as f: - f.write('{"device": "printer-test"}') - config = securedropexport.Metadata(temp_folder) - assert config.is_valid() - assert config.encryption_key is None - assert config.encryption_method is None - - -def test_valid_printer_config(capsys): - temp_folder = tempfile.mkdtemp() - metadata = os.path.join(temp_folder, securedropexport.Metadata.METADATA_FILE) - with open(metadata, "w") as f: - f.write('{"device": "printer"}') - config = securedropexport.Metadata(temp_folder) - assert config.is_valid() - assert config.encryption_key is None - assert config.encryption_method is None - - -def test_invalid_encryption_config(capsys): - temp_folder = tempfile.mkdtemp() - metadata = os.path.join(temp_folder, securedropexport.Metadata.METADATA_FILE) - with open(metadata, "w") as f: - f.write( - '{"device": "disk", "encryption_method": "base64", "encryption_key": "hunter1"}' - ) - config = securedropexport.Metadata(temp_folder) - assert config.encryption_key == "hunter1" - assert config.encryption_method == "base64" - assert not config.is_valid() - - -def test_valid_encryption_config(capsys): - temp_folder = tempfile.mkdtemp() - metadata = os.path.join(temp_folder, securedropexport.Metadata.METADATA_FILE) - with open(metadata, "w") as f: - f.write( - '{"device": "disk", "encryption_method": "luks", "encryption_key": "hunter1"}' - ) - config = securedropexport.Metadata(temp_folder) - assert config.encryption_key == "hunter1" - assert config.encryption_method == "luks" - assert config.is_valid() - - -@mock.patch("subprocess.check_call") -def test_popup_message(mocked_call): - securedropexport.popup_message("hello!") - mocked_call.assert_called_once_with([ - "notify-send", - "--expire-time", "3000", - "--icon", "/usr/share/securedrop/icons/sd-logo.png", - "SecureDrop: hello!" - ]) - - -@mock.patch("subprocess.check_output", return_value=SAMPLE_OUTPUT_BOTHER_PRINTER) -def test_get_good_printer_uri(mocked_call): - result = securedropexport.get_printer_uri() - assert result == "usb://Brother/HL-L2320D%20series?serial=A00000A000000" - - -@mock.patch("subprocess.check_output", return_value=SAMPLE_OUTPUT_NO_PRINTER) -def test_get_bad_printer_uri(mocked_call, capsys): - expected_message = "USB Printer not found" - mocked_exit = mock.patch("securedropexport.exit_gracefully", return_value=0) - - with pytest.raises(SystemExit) as sysexit: - result = securedropexport.get_printer_uri() - assert result == "" - mocked_exit.assert_called_once_with(expected_message) - - assert sysexit.value.code == 0 - captured = capsys.readouterr() - assert captured.err == "{}\n".format(expected_message) - assert captured.out == "" - - -@pytest.mark.parametrize('open_office_paths', [ - "/tmp/whatver/thisisadoc.doc" - "/home/user/Downloads/thisisadoc.xlsx" - "/home/user/Downloads/file.odt" - "/tmp/tmpJf83j9/secret.pptx" -]) -def test_is_open_office_file(capsys, open_office_paths): - assert securedropexport.is_open_office_file(open_office_paths) - - -@pytest.mark.parametrize('open_office_paths', [ - "/tmp/whatver/thisisadoc.doccc" - "/home/user/Downloads/thisisa.xlsx.zip" - "/home/user/Downloads/file.odz" - "/tmp/tmpJf83j9/secret.gpg" -]) -def test_is_not_open_office_file(capsys, open_office_paths): - assert not securedropexport.is_open_office_file(open_office_paths) diff --git a/tests/test_sd_export.py b/tests/test_sd_export.py index 588125680..a5078d486 100644 --- a/tests/test_sd_export.py +++ b/tests/test_sd_export.py @@ -20,6 +20,7 @@ def test_files_are_properly_copied(self): def test_sd_export_package_installed(self): self.assertTrue(self._package_is_installed("cryptsetup")) self.assertTrue(self._package_is_installed("printer-driver-brlaser")) + self.assertTrue(self._package_is_installed("securedrop-export")) def load_tests(loader, tests, pattern):