Skip to content

Commit

Permalink
Merge pull request #1 from freedomofpress/271-package-send-to-usb
Browse files Browse the repository at this point in the history
Adds modular version of Qubes Workstation send-to-usb script
  • Loading branch information
emkll authored Jul 23, 2019
2 parents 70a99e7 + 8806ea1 commit 21f2f34
Show file tree
Hide file tree
Showing 16 changed files with 625 additions and 0 deletions.
12 changes: 12 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
include LICENSE
include README.md
include securedrop_export/VERSION
include changelog.md
include build-requirements.txt
include requirements.txt
include securedrop_export/*.py
include setup.py
include files/send-to-usb.desktop
include files/application-x-sd-export.xml
include files/sd-logo.png

Empty file added build-requirements.txt
Empty file.
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
securedrop-export (0.1.1-1) unstable; urgency=medium

[ Freedom Of The Press Foundation ]
* Initial release

-- SecureDrop Team <[email protected]> Thu, 18 Jul 2019 10:47:38 -0700
7 changes: 7 additions & 0 deletions files/application-x-sd-export.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-sd-export">
<comment>Archive for transfering files from the SecureDrop workstation to an external USB device.</comment>
<glob pattern="*.sd-export"/>
</mime-type>
</mime-info>
Binary file added files/sd-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions files/send-to-usb.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[Desktop Entry]
Type=Application
MimeType=application/x-sd-export
Name="Export SD submission to USB"
Exec=/usr/bin/send-to-usb
Empty file added requirements.txt
Empty file.
1 change: 1 addition & 0 deletions securedrop_export/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.1
Empty file added securedrop_export/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions securedrop_export/entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
import shutil
import sys

from securedrop_export import export
from securedrop_export import main


def start():
my_sub = export.SDExport(sys.argv[1])
try:
# Halt immediately if target file is absent
if not os.path.exists(my_sub.archive):
msg = "File does not exist"
my_sub.exit_gracefully(msg)
main.__main__(my_sub)
# Delete extracted achive from tempfile
shutil.rmtree(my_sub.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:"
my_sub.exit_gracefully(msg, e=e)
327 changes: 327 additions & 0 deletions securedrop_export/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
#!/usr/bin/env python3

import datetime
import json
import os
import shutil
import signal
import subprocess
import sys
import tarfile
import tempfile
import time

PRINTER_NAME = "sdw-printer"
PRINTER_WAIT_TIMEOUT = 60
DEVICE = "/dev/sda1"
MOUNTPOINT = "/media/usb"
ENCRYPTED_DEVICE = "encrypted_volume"
BRLASER_DRIVER = "/usr/share/cups/drv/brlaser.drv"
BRLASER_PPD = "/usr/share/cups/model/br7030.ppd"


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:
raise

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


class SDExport(object):

def __init__(self, archive):
self.device = DEVICE
self.mountpoint = MOUNTPOINT
self.encrypted_device = ENCRYPTED_DEVICE

self.printer_name = PRINTER_NAME
self.printer_wait_timeout = PRINTER_WAIT_TIMEOUT

self.brlaser_driver = BRLASER_DRIVER
self.brlaser_ppd = BRLASER_PPD

self.archive = archive
self.submission_dirname = os.path.basename(self.archive).split(".")[0]
self.target_dirname = "sd-export-{}".format(
datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
)
self.tmpdir = tempfile.mkdtemp()


def exit_gracefully(self, 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(self.tmpdir):
shutil.rmtree(self.tmpdir)
e_output = e.output
except Exception:
e_output = "<unknown exception>"
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
self.popup_message("Export error: {}".format(msg))
sys.exit(0)


def popup_message(self, 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:"
self.exit_gracefully(msg, e=e)


def extract_tarball(self):
try:
with tarfile.open(self.archive) as tar:
tar.extractall(self.tmpdir)
except Exception as e:
print (e)
msg = "Error opening export bundle: "
self.exit_gracefully(msg, e=e)


def unlock_luks_volume(self, encryption_key):
# the luks device is not already unlocked
if not os.path.exists(os.path.join("/dev/mapper/", self.encrypted_device)):
p = subprocess.Popen(
["sudo", "cryptsetup", "luksOpen", self.device, self.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."
self.exit_gracefully(msg)


def mount_volume(self):
# mount target not created
if not os.path.exists(self.mountpoint):
subprocess.check_call(["sudo", "mkdir", self.mountpoint])
try:
subprocess.check_call(
[
"sudo",
"mount",
os.path.join("/dev/mapper/", self.encrypted_device),
self.mountpoint,
]
)
subprocess.check_call(
[
"sudo",
"chown",
"-R", "user:user", self.mountpoint,
]
)
except subprocess.CalledProcessError as e:
# clean up
subprocess.check_call(["sudo", "cryptsetup", "luksClose", self.encrypted_device])
msg = "An error occurred while mounting disk: "
self.exit_gracefully(msg, e=e)


def copy_submission(self):
# move files to drive (overwrites files with same filename) and unmount drive
try:
target_path = os.path.join(self.mountpoint, self.target_dirname)
subprocess.check_call(["mkdir", target_path])
export_data = os.path.join(
self.tmpdir, "export_data/"
)
subprocess.check_call(["cp", "-r", export_data, target_path])
self.popup_message("Files exported successfully to disk.")
except (subprocess.CalledProcessError, OSError) as e:
msg = "Error writing to disk:"
self.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", self.mountpoint])
subprocess.check_call(["sudo", "cryptsetup", "luksClose", self.encrypted_device])
subprocess.check_call(["rm", "-rf", self.tmpdir])
sys.exit(0)


def wait_for_print(self):
# 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(self.printer_wait_timeout)
printer_idle_string = "printer {} is idle".format(self.printer_name)
while(True):
try:
output = subprocess.check_output(["lpstat", "-p", self.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"
self.exit_gracefully(msg, e=e)
except TimeoutException as e:
msg = "Timeout when getting printer information"
self.exit_gracefully(msg, e=e)
return True


def get_printer_uri(self):
# 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."
self.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
self.exit_gracefully("USB Printer not found")
elif "Brother" in printer_uri:
return printer_uri
else:
# printer url is a make that is unsupported
self.exit_gracefully("USB Printer not supported")


def install_printer_ppd(self, 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", self.brlaser_driver, "-d", "/usr/share/cups/model/"]
)
except subprocess.CalledProcessError as e:
msg = "Error installing ppd file for printer {}.".format(uri)
self.exit_gracefully(msg, e=e)
return self.brlaser_ppd
# Here, we could support ppd drivers for other makes or models in the future


def setup_printer(self, printer_uri, printer_ppd):
try:
# Add the printer using lpadmin
subprocess.check_call(
[
"sudo",
"lpadmin",
"-p",
self.printer_name,
"-v",
printer_uri,
"-P",
printer_ppd,
]
)
# Activate the printer so that it can receive jobs
subprocess.check_call(["sudo", "lpadmin", "-p", self.printer_name, "-E"])
# Allow user to print (without using sudo)
subprocess.check_call(
["sudo", "lpadmin", "-p", self.printer_name, "-u", "allow:user"]
)
except subprocess.CalledProcessError as e:
msg = "Error setting up printer {} at {} using {}.".format(
self.printer_name, printer_uri, printer_ppd
)
self.exit_gracefully(msg, e=e)


def print_test_page(self):
self.print_file("/usr/share/cups/data/testprint")
self.popup_message("Printing test page")


def print_all_files(self):
files_path = os.path.join(self.tmpdir, "export_data/")
files = os.listdir(files_path)
print_count = 0
for f in files:
file_path = os.path.join(files_path, f)
self.print_file(file_path)
print_count += 1
msg = "Printing document {} of {}".format(print_count, len(files))
self.popup_message(msg)


def is_open_office_file(self, 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(self, 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 self.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", self.printer_name, file_to_print])
except subprocess.CalledProcessError as e:
msg = "Error printing file {} with printer {}.".format(
file_to_print, self.printer_name
)
self.exit_gracefully(msg, e=e)

## class ends here
class TimeoutException(Exception):
pass


def handler(s, f):
raise TimeoutException("Timeout")
Loading

0 comments on commit 21f2f34

Please sign in to comment.