diff --git a/BUILD.md b/BUILD.md index b1f7ae6..ce7d99d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -31,7 +31,13 @@ Create a .deb: Install dependencies: ```sh -sudo dnf install -y rpm-build podman python3 python3-setuptools python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-requests python3-colorama +sudo dnf install -y rpm-build podman python3 python3-setuptools python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-requests python3-colorama python3-psutil +``` + +Build the latest container: + +```sh +./install/linux/build-container.py ``` Run from source tree: @@ -62,7 +68,9 @@ If you don't have it already, install poetry (`pip3 install --user poetry`). The poetry install ``` -Make sure [Docker Desktop](https://www.docker.com/products/docker-desktop) and vagrant (`brew install vagrant`) are installed and run this to collect the binaries from Docker Desktop and then build a custom Alpine Linux ISO for Dangerzone, and copy them into the `share` folder: +Make sure [Docker Desktop](https://www.docker.com/products/docker-desktop) and vagrant (`brew install vagrant`) are installed. + +Run this to build a custom Alpine Linux ISO for Dangerzone, and copy it into the `share` folder: ```sh ./install/macos/make-vm.sh @@ -97,8 +105,6 @@ The output is in the `dist` folder. ## Windows -Install [Docker Desktop](https://www.docker.com/products/docker-desktop). - These instructions include adding folders to the path in Windows. To do this, go to Start and type "advanced system settings", and open "View advanced system settings" in the Control Panel. Click Environment Variables. Under "System variables" double-click on Path. From there you can add and remove folders that are available in the PATH. Download Python 3.9.0, 32-bit (x86) from https://www.python.org/downloads/release/python-390/. I downloaded python-3.9.0.exe. When installing it, make sure to check the "Add Python 3.9 to PATH" checkbox on the first page of the installer. @@ -115,6 +121,18 @@ Change to the `dangerzone` folder, and install the poetry dependencies: poetry install ``` +Make sure these are installed: + +- [Docker Desktop](https://www.docker.com/products/docker-desktop) +- [Vagrant](https://www.vagrantup.com/downloads) +- [VirtualBox](https://www.virtualbox.org/wiki/Downloads) + +Run this to build a custom Alpine Linux ISO for Dangerzone, and copy it (and some binaries from Docker) into the `share` folder: + +``` +.\install\windows\make-vm.bat +``` + After that you can launch dangerzone during development with: ``` diff --git a/dangerzone/container.py b/dangerzone/container.py index f5a636c..8d1fa9d 100644 --- a/dangerzone/container.py +++ b/dangerzone/container.py @@ -88,10 +88,13 @@ def vm_exec(args, vm_info, stdout_callback=None): return exec(args, stdout_callback) -def vm_mkdir(vm_info): +def vm_mkdirs(vm_info): guest_path = os.path.join("/home/user/", str(uuid.uuid4())) - vm_exec(["/bin/mkdir", guest_path], vm_info) - return guest_path + input_dir = os.path.join(guest_path, "input") + pixel_dir = os.path.join(guest_path, "pixel") + safe_dir = os.path.join(guest_path, "safe") + vm_exec(["/bin/mkdir", guest_path, input_dir, pixel_dir, safe_dir], vm_info) + return guest_path, input_dir, pixel_dir, safe_dir def vm_rmdir(guest_path, vm_info): @@ -154,13 +157,9 @@ def convert(global_common, input_filename, output_filename, ocr_lang, stdout_cal # Otherwise, create temp dirs if vm_info: ssh_args_str = " ".join(pipes.quote(s) for s in vm_ssh_args(vm_info)) - print("If you want to SSH to the VM: " + ssh_args_str) - - guest_tmpdir = vm_mkdir(vm_info) - input_dir = os.path.join(guest_tmpdir, "input") - pixel_dir = os.path.join(guest_tmpdir, "pixel") - safe_dir = os.path.join(guest_tmpdir, "safe") + print("\nIf you want to SSH to the VM:\n" + ssh_args_str + "\n") + guest_tmpdir, input_dir, pixel_dir, safe_dir = vm_mkdirs(vm_info) guest_input_filename = os.path.join(input_dir, "input_file") container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf") diff --git a/dangerzone/gui/common.py b/dangerzone/gui/common.py index f29d8e3..362a2fe 100644 --- a/dangerzone/gui/common.py +++ b/dangerzone/gui/common.py @@ -38,6 +38,9 @@ class GuiCommon(object): # Preload list of PDF viewers on computer self.pdf_viewers = self._find_pdf_viewers() + # Are we done waiting (for VM to start, or container to install) + self.is_waiting_finished = False + def get_window_icon(self): if platform.system() == "Windows": path = self.global_common.get_resource_path("dangerzone.ico") diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 0378b67..27a2151 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -54,8 +54,12 @@ class MainWindow(QtWidgets.QMainWindow): self.content_widget.close_window.connect(self.close) # Only use the waiting widget if we have a VM - self.waiting_widget.show() - self.content_widget.hide() + if self.gui_common.is_waiting_finished: + self.waiting_widget.hide() + self.content_widget.show() + else: + self.waiting_widget.show() + self.content_widget.hide() # Layout layout = QtWidgets.QVBoxLayout() @@ -70,6 +74,7 @@ class MainWindow(QtWidgets.QMainWindow): self.show() def waiting_finished(self): + self.gui_common.is_waiting_finished = True self.waiting_widget.hide() self.content_widget.show() @@ -423,13 +428,14 @@ class SettingsWidget(QtWidgets.QWidget): class ConvertThread(QtCore.QThread): - finished = QtCore.Signal() + finished = QtCore.Signal(bool) update = QtCore.Signal(bool, str, int) def __init__(self, global_common, common): super(ConvertThread, self).__init__() self.global_common = global_common self.common = common + self.error = False def run(self): ocr_lang = self.global_common.ocr_languages[ @@ -443,19 +449,20 @@ class ConvertThread(QtCore.QThread): ocr_lang, self.stdout_callback, ): - self.finished.emit() + self.finished.emit(self.error) def stdout_callback(self, line): try: status = json.loads(line) except: print(f"Invalid JSON returned from container: {line}") - + self.error = True self.update.emit(True, "Invalid JSON returned from container", 0) return s = Style.BRIGHT + Fore.CYAN + f"{status['percentage']}% " if status["error"]: + self.error = True s += Style.RESET_ALL + Fore.RED + status["text"] else: s += Style.RESET_ALL + status["text"] @@ -473,6 +480,8 @@ class ConvertWidget(QtWidgets.QWidget): self.gui_common = gui_common self.common = common + self.error = False + # Dangerous document label self.dangerous_doc_label = QtWidgets.QLabel() self.dangerous_doc_label.setAlignment(QtCore.Qt.AlignCenter) @@ -480,11 +489,25 @@ class ConvertWidget(QtWidgets.QWidget): "QLabel { font-size: 16px; font-weight: bold; }" ) + # Label + self.error_image = QtWidgets.QLabel() + self.error_image.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(self.global_common.get_resource_path("error.png")) + ) + ) + self.error_image.hide() + self.label = QtWidgets.QLabel() self.label.setAlignment(QtCore.Qt.AlignCenter) self.label.setWordWrap(True) self.label.setStyleSheet("QLabel { font-size: 18px; }") + label_layout = QtWidgets.QHBoxLayout() + label_layout.addWidget(self.error_image) + label_layout.addWidget(self.label, stretch=1) + + # Progress bar self.progress = QtWidgets.QProgressBar() self.progress.setRange(0, 100) self.progress.setValue(0) @@ -493,7 +516,7 @@ class ConvertWidget(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.dangerous_doc_label) layout.addStretch() - layout.addWidget(self.label) + layout.addLayout(label_layout) layout.addWidget(self.progress) layout.addStretch() self.setLayout(layout) @@ -512,13 +535,17 @@ class ConvertWidget(QtWidgets.QWidget): def update(self, error, text, percentage): if error: - # TODO: add error image or something - pass + self.error = True + self.error_image.show() + self.progress.hide() self.label.setText(text) self.progress.setValue(percentage) def all_done(self): + if self.error: + return + # In Windows, open Explorer with the safe PDF in focus if platform.system() == "Windows": dest_filename_windows = self.common.output_filename.replace("/", "\\") diff --git a/dangerzone/gui/vm.py b/dangerzone/gui/vm.py index e69a7ab..17b412a 100644 --- a/dangerzone/gui/vm.py +++ b/dangerzone/gui/vm.py @@ -1,5 +1,4 @@ import os -import sys import subprocess import uuid import pipes @@ -10,6 +9,8 @@ import getpass import json import psutil import time +import platform +import shutil from PySide2 import QtCore @@ -32,24 +33,8 @@ class Vm(QtCore.QObject): self.sshd_port = None self.sshd_tunnel_port = None - # Processes - self.vpnkit_p = None - self.hyperkit_p = None - self.devnull = open(os.devnull, "w") - - # Relevant paths - self.vpnkit_path = self.global_common.get_resource_path("bin/vpnkit") - self.hyperkit_path = self.global_common.get_resource_path("bin/hyperkit") - self.vm_iso_path = self.global_common.get_resource_path("vm/dangerzone.iso") - self.vm_kernel_path = self.global_common.get_resource_path("vm/kernel") - self.vm_initramfs_path = self.global_common.get_resource_path( - "vm/initramfs.img" - ) - # Folder to hold temporary files related to the VM self.state_dir = tempfile.TemporaryDirectory() - self.vpnkit_sock_path = os.path.join(self.state_dir.name, "vpnkit.eth.sock") - self.hyperkit_pid_path = os.path.join(self.state_dir.name, "hyperkit.pid") self.ssh_host_key_path = os.path.join(self.state_dir.name, "host_ed25519") self.ssh_host_pubkey_path = os.path.join( self.state_dir.name, "host_ed25519.pub" @@ -63,11 +48,46 @@ class Vm(QtCore.QObject): self.vm_info_path = os.path.join(self.state_dir.name, "info.json") self.vm_disk_img_path = os.path.join(self.state_dir.name, "disk.img") - # UDID for VM - self.vm_uuid = str(uuid.uuid4()) - self.vm_cmdline = ( - "earlyprintk=serial console=ttyS0 modules=loop,squashfs,sd-mod" - ) + self.vm_iso_path = self.global_common.get_resource_path("vm/dangerzone.iso") + + if platform.system() == "Darwin": + self.ssh_keygen_path = shutil.which("ssh-keygen") + self.sshd_path = shutil.which("sshd") + + # Processes + self.vpnkit_p = None + self.hyperkit_p = None + self.devnull = open(os.devnull, "w") + + # Relevant paths + self.vpnkit_path = self.global_common.get_resource_path("bin/vpnkit") + self.hyperkit_path = self.global_common.get_resource_path("bin/hyperkit") + self.vm_kernel_path = self.global_common.get_resource_path("vm/kernel") + self.vm_initramfs_path = self.global_common.get_resource_path( + "vm/initramfs.img" + ) + + # Temporary files related to the VM + self.vpnkit_sock_path = os.path.join(self.state_dir.name, "vpnkit.eth.sock") + self.hyperkit_pid_path = os.path.join(self.state_dir.name, "hyperkit.pid") + + # UDID for VM + self.vm_uuid = str(uuid.uuid4()) + self.vm_cmdline = ( + "earlyprintk=serial console=ttyS0 modules=loop,squashfs,sd-mod" + ) + + if platform.system() == "Windows": + self.vboxmanage_path = ( + "C:\\Program Files\\Oracle\\VirtualBox\\VBoxManage.exe" + ) + + self.ssh_keygen_path = os.path.join( + self.global_common.get_resource_path("bin"), "ssh-keygen.exe" + ) + self.sshd_path = os.path.join( + self.global_common.get_resource_path("bin"), "sshd.exe" + ) # Threads self.wait_t = None @@ -76,8 +96,7 @@ class Vm(QtCore.QObject): self.stop() def start(self): - self.state = self.STATE_STARTING - self.vm_state_change.emit(self.state) + print("Starting VM\n") # Delete keys if they already exist for filename in [ @@ -89,10 +108,14 @@ class Vm(QtCore.QObject): if os.path.exists(filename): os.remove(filename) + # Find an open port + self.sshd_port = self.find_open_port() + self.sshd_tunnel_port = self.find_open_port() + # Generate new keys subprocess.run( [ - "/usr/bin/ssh-keygen", + self.ssh_keygen_path, "-t", "ed25519", "-C", @@ -107,7 +130,7 @@ class Vm(QtCore.QObject): ) subprocess.run( [ - "/usr/bin/ssh-keygen", + self.ssh_keygen_path, "-t", "ed25519", "-C", @@ -125,13 +148,9 @@ class Vm(QtCore.QObject): with open(self.ssh_client_pubkey_path) as f: ssh_client_pubkey = f.read() - # Find an open port - self.sshd_port = self.find_open_port() - self.sshd_tunnel_port = self.find_open_port() - # Start an sshd service on this port args = [ - "/usr/sbin/sshd", + self.sshd_path, "-4", "-E", self.sshd_log_path, @@ -160,6 +179,16 @@ class Vm(QtCore.QObject): print("> " + args_str) subprocess.run(args, stdout=self.devnull, stderr=self.devnull) + if platform.system() == "Darwin": + self.start_macos() + + if platform.system() == "Windows": + self.start_windows() + + def start_macos(self): + self.state = self.STATE_STARTING + self.vm_state_change.emit(self.state) + # Create a JSON object to pass into the VM # This is a 512kb file that starts with a JSON object, followed by null bytes guest_vm_info = { @@ -246,6 +275,84 @@ class Vm(QtCore.QObject): self.wait_t.timeout.connect(self.vm_timeout) self.wait_t.start() + def start_windows(self): + vm_name = "dangezone-podman" + basefolder_path = os.path.join( + self.global_common.appdata_path, "virtualbox-basefolder" + ) + + # See if we already have a VM + exists = False + for line in subprocess.check_output([self.vboxmanage_path, "list", "vms"]): + name = line.split()[0].lstrip('"').rstrip('"') + if name == vm_name: + exists = True + break + + # Create the VM + if not exists: + subprocess.run( + [ + self.vboxmanage_path, + "createvm", + "--name", + vm_name, + "--basefolder", + basefolder_path, + "--ostype", + "Linux_x64", + "--register", + ] + ) + + # Configure the VM + subprocess.run( + [ + self.vboxmanage_path, + "modifyvm", + vm_name, + "--memory", + "4096", + "--nic1", + "nat", + "--cableconnected1", + "on", + ] + ) + subprocess.run( + [ + self.vboxmanage_path, + "storagectl", + vm_name, + "--name", + "DangerzoneBoot", + "--add", + "ide", + "--bootable", + "on", + ] + ) + subprocess.run( + [ + self.vboxmanage_path, + "storageattach", + vm_name, + "--storagectl", + "DangerzoneBoot", + "--port", + "1", + "--device", + "1", + "--type", + "dvddrive", + "--medium", + self.vm_iso_path, + ] + ) + + # Start the VM + subprocess.run([self.vboxmanage_path, "startvm", "--type", "headless"]) + def vm_connected(self): self.state = self.STATE_ON self.vm_state_change.emit(self.state) @@ -255,8 +362,16 @@ class Vm(QtCore.QObject): self.vm_state_change.emit(self.state) def stop(self): - # Kill existing processes self.kill_sshd() + + if platform.system() == "Darwin": + self.stop_macos() + + if platform.system() == "Windows": + self.stop_windows() + + def stop_macos(self): + # Kill existing processes if self.vpnkit_p is not None: self.vpnkit_p.terminate() self.vpnkit_p = None @@ -267,6 +382,17 @@ class Vm(QtCore.QObject): # Just to be extra sure self.kill_hyperkit() + def stop_windows(self): + vm_name = "dangezone-podman" + subprocess.run( + [ + self.vboxmanage_path, + "controlvm", + vm_name, + "poweroff", + ] + ) + def find_open_port(self): with socket.socket() as tmpsock: while True: diff --git a/dev_scripts/dangerzone-container.bat b/dev_scripts/dangerzone-container.bat deleted file mode 100644 index 07b3fc3..0000000 --- a/dev_scripts/dangerzone-container.bat +++ /dev/null @@ -1,2 +0,0 @@ -set DANGERZONE_MODE=container -poetry run python .\dev_scripts\dangerzone %* \ No newline at end of file diff --git a/install/vm-builder/windows.sh b/install/vm-builder/windows.sh new file mode 100644 index 0000000..1bfaa1d --- /dev/null +++ b/install/vm-builder/windows.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +VAGRANT_FILES=$(find /vagrant -type f | grep -v /vagrant/.vagrant | grep -v /vagrant/vm) +DANGERZONE_CONVERTER_FILES=$(find /opt/dangerzone-converter -type f) + +for FILE in $VAGRANT_FILES; do dos2unix $FILE; done +for FILE in $DANGERZONE_CONVERTER_FILES; do dos2unix $FILE; done + +/vagrant/build-iso.sh + +for FILE in $VAGRANT_FILES; do unix2dos $FILE; done +for FILE in $DANGERZONE_CONVERTER_FILES; do unix2dos $FILE; done diff --git a/install/windows/get-openssh.py b/install/windows/get-openssh.py new file mode 100644 index 0000000..ce24f1c --- /dev/null +++ b/install/windows/get-openssh.py @@ -0,0 +1,65 @@ +import os +import sys +import inspect +import requests +import hashlib +import zipfile +import shutil + + +def main(): + zip_url = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/V8.6.0.0p1-Beta/OpenSSH-Win32.zip" + expected_zip_sha256 = ( + "0221324212413a6caf260f95e308d22f8c141fc37727b622a6ad50998c46d226" + ) + + # Figure out the paths + root_path = os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + ) + ) + zip_path = os.path.join(root_path, "build", "OpenSSH-Win32.zip") + extracted_path = os.path.join(root_path, "build", "OpenSSH-Win32") + bin_path = os.path.join(root_path, "share", "bin") + + os.makedirs(os.path.join(root_path, "build"), exist_ok=True) + os.makedirs(os.path.join(bin_path), exist_ok=True) + + # Make sure openssh is downloaded + if not os.path.exists(zip_path): + print(f"Downloading {zip_url}") + r = requests.get(zip_url) + open(zip_path, "wb").write(r.content) + zip_sha256 = hashlib.sha256(r.content).hexdigest() + else: + zip_data = open(zip_path, "rb").read() + zip_sha256 = hashlib.sha256(zip_data).hexdigest() + + # Compare the hash + if zip_sha256 != expected_zip_sha256: + print("ERROR! The sha256 doesn't match:") + print("expected: {}".format(expected_zip_sha256)) + print(" actual: {}".format(zip_sha256)) + sys.exit(-1) + + # Extract the zip + with zipfile.ZipFile(zip_path, "r") as z: + z.extractall(os.path.join(root_path, "build")) + + # Copy binaries to share + shutil.copy(os.path.join(extracted_path, "libcrypto.dll"), bin_path) + shutil.copy(os.path.join(extracted_path, "moduli"), bin_path) + shutil.copy(os.path.join(extracted_path, "scp.exe"), bin_path) + shutil.copy(os.path.join(extracted_path, "ssh-agent.exe"), bin_path) + shutil.copy(os.path.join(extracted_path, "ssh-keygen.exe"), bin_path) + shutil.copy(os.path.join(extracted_path, "ssh.exe"), bin_path) + shutil.copy(os.path.join(extracted_path, "sshd.exe"), bin_path) + shutil.copyfile( + os.path.join(extracted_path, "LICENSE.txt"), + os.path.join(bin_path, "LICENSE-OpenSSH.txt"), + ) + + +if __name__ == "__main__": + main() diff --git a/install/windows/make-vm.bat b/install/windows/make-vm.bat new file mode 100644 index 0000000..95def86 --- /dev/null +++ b/install/windows/make-vm.bat @@ -0,0 +1,11 @@ +REM Build ISO +cd install\vm-builder +vagrant up +vagrant ssh -- dos2unix /vagrant/windows.sh +vagrant ssh -- /vagrant/windows.sh +vagrant halt +cd ..\.. + +REM Copy the ISO to resources +mkdir share\vm +cp install\vm-builder\vm\dangerzone.iso share\vm diff --git a/share/error.png b/share/error.png new file mode 100644 index 0000000..270bd24 Binary files /dev/null and b/share/error.png differ