diff --git a/dom0/sd-export-files.sls b/dom0/sd-export-files.sls
index 30fed69f..f3b7440f 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,50 +20,10 @@ 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
-
-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
+# Install securedrop-export package https://github.com/freedomofpress/securedrop-export
+sd-export-install-package:
+ pkg.installed:
+ - name: securedrop-export
# populate sd-export-config.json in sd-export-template. This contains the usb
# device information used for the export
diff --git a/sd-export/application-x-sd-export.xml b/sd-export/application-x-sd-export.xml
deleted file mode 100644
index 9e36ef08..00000000
--- 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 1f5969f8..00000000
--- 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 6521a92c..00000000
--- 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 52af5467..00000000
--- 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/base.py b/tests/base.py
index 13e52765..200d8e63 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -98,3 +98,10 @@ def assertFileHasLine(self, remote_path, wanted_line):
msg = "File {} does not contain expected line {}".format(remote_path,
wanted_line)
raise AssertionError(msg)
+
+ def _fileExists(self, remote_path):
+ # ls will return non-zero if the file doesn't exists
+ # and error will be propagated to the unittest
+ subprocess.check_call(["qvm-run", "-a", "-q", self.vm_name,
+ "ls {}".format(remote_path)])
+ return True
diff --git a/tests/test_sd_export.py b/tests/test_sd_export.py
index 35cbaaec..50015c88 100644
--- a/tests/test_sd_export.py
+++ b/tests/test_sd_export.py
@@ -12,16 +12,14 @@ def setUp(self):
super(SD_Export_Tests, self).setUp()
def test_files_are_properly_copied(self):
- self.assertFilesMatch("/usr/bin/send-to-usb",
- "sd-export/send-to-usb")
- self.assertFilesMatch("/usr/share/applications/send-to-usb.desktop",
- "sd-export/send-to-usb.desktop")
- self.assertFilesMatch("/usr/share/mime/packages/application-x-sd-export.xml", # noqa
- "sd-export/application-x-sd-export.xml")
+ self.assertTrue(self._fileExists("/usr/bin/send-to-usb"))
+ self.assertTrue(self._fileExists("/usr/share/applications/send-to-usb.desktop"))
+ self.assertTrue(self._fileExists("/usr/share/mime/packages/application-x-sd-export.xml"))
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 test_sd_export_config_present(self):
with open("config.json") as c: