Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial printing support for sd-export-usb #277

Merged
merged 6 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ qvm-copy-to-vm sd-export-usb ~/.securedrop_client/data/name-of-file

The development plan is to provide functionality in the *SecureDrop Client* that automates step 3, and assists the user in taking these steps via GUI prompts. Eventually we plan to provide other methods for export, such as [OnionShare](https://onionshare.org/) (this will require the attachment of a NetVM), using a dedicated export VM template with tools such as OnionShare and Veracrypt. The next section includes instructions to approximate the OnionShare sharing flow.

##### Automated export flow (Work in progress, client integration TBD)
##### Automated export flows

The `sd-export-usb` disposable VM handles exports to USB devices through `qvm-open-in-vm`. USB device IDs are configured in `config.json`. The automated export flows make use of the `qvm-usb --persistent` feature. This means that the persistent USB device must be available for `sd-export-usb` to start. In other words, a USB memory stick or a printer must be connected **prior** to the the `qvm-open-in-vm sd-export-usb` call is made.
eloquence marked this conversation as resolved.
Show resolved Hide resolved

###### Automated encrypted USB export flow (Work in progress, client integration TBD)

The SecureDrop Workstation can automatically export to a luks-encrypted USB device provided the correct format. The file extension of the tar archive must be `.sd-export`, containing the following structure:

Expand All @@ -200,11 +204,46 @@ The folder `export_data` contains all the files that will be exported to the dis

```
{
"device": "disk"
"encryption-method": "luks"
"encryption-key": "Your encryption passhrase goes here"
}
```

###### Automated printing flow (Work in progress, client integration TBD)

The SecureDrop Workstation can automatically print files to a USB-connected printer provided the correct format. The file extension of the tar archive must be `.sd-export`, containing the following structure:

Note that only Brother printers are supported now (tested with HL-L2320D)


```
.
├── metadata.json
└── export_data
├── file-to-export-1.txt
├── file-to-export-2.pdf
├── file-to-export-3.doc
[...]
```

The folder `export_data` contains all the files that will be printed, and the file `metadata.json` contains an instruction indicating that the archive will be printed:

```
{
"device": "printer"
}
```

Optionally you can use the `printer-test` device to send a printer test page and ensure the printer is functional

```
{
"device": "printer-test"
}
```


###### Create the transfer device

You can find instructions to create a luks-encrypted transfer device in the [SecureDrop docs](https://docs.securedrop.org/en/latest/set_up_transfer_device.html).
Expand Down
13 changes: 12 additions & 1 deletion dom0/sd-export-files.sls
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
include:
- fpf-apt-test-repo

sd-export-template-install-cryptsetup:
sd-export-template-install-packages:
pkg.installed:
- pkgs:
- cryptsetup
- cups
- task-print-server

sd-export-send-to-usb-script:
file.managed:
Expand Down Expand Up @@ -51,3 +53,12 @@ sd-export-file-format:
- 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
199 changes: 173 additions & 26 deletions sd-export/send-to-usb
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,62 @@ def exit_gracefully(msg, e=False):
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 extract_tarball():
def popup_message(msg):
try:
with tarfile.open(SUBMISSION_ARCHIVE) as tar:
tar.extractall(SUBMISSION_TMPDIR)
except Exception as e:
msg = "Error opening export bundle: "
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)


def retrieve_metadata():
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:
metadata_filepath = os.path.join(SUBMISSION_TMPDIR, SUBMISSION_DIRNAME, "metadata.json")
with open(metadata_filepath) as json_data:
data = json.load(json_data)
encryption_method = data["encryption-method"]
encryption_key = data["encryption-key"]
with tarfile.open(SUBMISSION_ARCHIVE) as tar:
tar.extractall(SUBMISSION_TMPDIR)
except Exception as e:
msg = "Error parsing metadata."
msg = "Error opening export bundle: "
exit_gracefully(msg, e=e)

# we only support luks for now
if encryption_method != "luks":
msg = "Unsupported export encryption."
exit_gracefully(msg)

return (encryption_method, encryption_key)


def unlock_luks_volume(encryption_key):
# the luks device is not already unlocked
Expand Down Expand Up @@ -84,7 +110,7 @@ def mount_volume():
"sudo",
"mount",
os.path.join("/dev/mapper/", ENCRYPTED_DEVICE),
MOUNTPOINT
MOUNTPOINT,
]
)
subprocess.check_call(["sudo", "chown", "-R", "user:user", MOUNTPOINT])
Expand All @@ -101,8 +127,11 @@ def copy_submission():
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/")
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)
Expand All @@ -116,12 +145,125 @@ def copy_submission():
sys.exit(0)


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 print_file(printer_name, file_to_print):
try:
subprocess.check_call(["lpr", "-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()
encryption_method, encryption_key = retrieve_metadata()
unlock_luks_volume(encryption_key)
mount_volume()
copy_submission()

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__":
Expand All @@ -133,14 +275,19 @@ if __name__ == "__main__":
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"

# 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"))
TARGET_DIRNAME = "sd-export-{}".format(
datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
)
SUBMISSION_TMPDIR = tempfile.mkdtemp()

main()
Expand Down
1 change: 1 addition & 0 deletions sd-export/test-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest
Loading