Merge branch 'vm' of github.com:firstlookmedia/dangerzone into vm

This commit is contained in:
Micah Lee 2021-08-11 15:50:29 -07:00
commit db674a184e
No known key found for this signature in database
GPG key ID: 403C2657CD994F73
10 changed files with 314 additions and 55 deletions

View file

@ -31,7 +31,13 @@ Create a .deb:
Install dependencies: Install dependencies:
```sh ```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: Run from source tree:
@ -62,7 +68,9 @@ If you don't have it already, install poetry (`pip3 install --user poetry`). The
poetry install 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 ```sh
./install/macos/make-vm.sh ./install/macos/make-vm.sh
@ -97,8 +105,6 @@ The output is in the `dist` folder.
## Windows ## 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. 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. 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 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: After that you can launch dangerzone during development with:
``` ```

View file

@ -88,10 +88,13 @@ def vm_exec(args, vm_info, stdout_callback=None):
return exec(args, stdout_callback) 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())) guest_path = os.path.join("/home/user/", str(uuid.uuid4()))
vm_exec(["/bin/mkdir", guest_path], vm_info) input_dir = os.path.join(guest_path, "input")
return guest_path 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): 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 # Otherwise, create temp dirs
if vm_info: if vm_info:
ssh_args_str = " ".join(pipes.quote(s) for s in vm_ssh_args(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) print("\nIf you want to SSH to the VM:\n" + ssh_args_str + "\n")
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")
guest_tmpdir, input_dir, pixel_dir, safe_dir = vm_mkdirs(vm_info)
guest_input_filename = os.path.join(input_dir, "input_file") guest_input_filename = os.path.join(input_dir, "input_file")
container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf") container_output_filename = os.path.join(safe_dir, "safe-output-compressed.pdf")

View file

@ -38,6 +38,9 @@ class GuiCommon(object):
# Preload list of PDF viewers on computer # Preload list of PDF viewers on computer
self.pdf_viewers = self._find_pdf_viewers() 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): def get_window_icon(self):
if platform.system() == "Windows": if platform.system() == "Windows":
path = self.global_common.get_resource_path("dangerzone.ico") path = self.global_common.get_resource_path("dangerzone.ico")

View file

@ -54,8 +54,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.content_widget.close_window.connect(self.close) self.content_widget.close_window.connect(self.close)
# Only use the waiting widget if we have a VM # Only use the waiting widget if we have a VM
self.waiting_widget.show() if self.gui_common.is_waiting_finished:
self.content_widget.hide() self.waiting_widget.hide()
self.content_widget.show()
else:
self.waiting_widget.show()
self.content_widget.hide()
# Layout # Layout
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
@ -70,6 +74,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.show() self.show()
def waiting_finished(self): def waiting_finished(self):
self.gui_common.is_waiting_finished = True
self.waiting_widget.hide() self.waiting_widget.hide()
self.content_widget.show() self.content_widget.show()
@ -423,13 +428,14 @@ class SettingsWidget(QtWidgets.QWidget):
class ConvertThread(QtCore.QThread): class ConvertThread(QtCore.QThread):
finished = QtCore.Signal() finished = QtCore.Signal(bool)
update = QtCore.Signal(bool, str, int) update = QtCore.Signal(bool, str, int)
def __init__(self, global_common, common): def __init__(self, global_common, common):
super(ConvertThread, self).__init__() super(ConvertThread, self).__init__()
self.global_common = global_common self.global_common = global_common
self.common = common self.common = common
self.error = False
def run(self): def run(self):
ocr_lang = self.global_common.ocr_languages[ ocr_lang = self.global_common.ocr_languages[
@ -443,19 +449,20 @@ class ConvertThread(QtCore.QThread):
ocr_lang, ocr_lang,
self.stdout_callback, self.stdout_callback,
): ):
self.finished.emit() self.finished.emit(self.error)
def stdout_callback(self, line): def stdout_callback(self, line):
try: try:
status = json.loads(line) status = json.loads(line)
except: except:
print(f"Invalid JSON returned from container: {line}") print(f"Invalid JSON returned from container: {line}")
self.error = True
self.update.emit(True, "Invalid JSON returned from container", 0) self.update.emit(True, "Invalid JSON returned from container", 0)
return return
s = Style.BRIGHT + Fore.CYAN + f"{status['percentage']}% " s = Style.BRIGHT + Fore.CYAN + f"{status['percentage']}% "
if status["error"]: if status["error"]:
self.error = True
s += Style.RESET_ALL + Fore.RED + status["text"] s += Style.RESET_ALL + Fore.RED + status["text"]
else: else:
s += Style.RESET_ALL + status["text"] s += Style.RESET_ALL + status["text"]
@ -473,6 +480,8 @@ class ConvertWidget(QtWidgets.QWidget):
self.gui_common = gui_common self.gui_common = gui_common
self.common = common self.common = common
self.error = False
# Dangerous document label # Dangerous document label
self.dangerous_doc_label = QtWidgets.QLabel() self.dangerous_doc_label = QtWidgets.QLabel()
self.dangerous_doc_label.setAlignment(QtCore.Qt.AlignCenter) self.dangerous_doc_label.setAlignment(QtCore.Qt.AlignCenter)
@ -480,11 +489,25 @@ class ConvertWidget(QtWidgets.QWidget):
"QLabel { font-size: 16px; font-weight: bold; }" "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 = QtWidgets.QLabel()
self.label.setAlignment(QtCore.Qt.AlignCenter) self.label.setAlignment(QtCore.Qt.AlignCenter)
self.label.setWordWrap(True) self.label.setWordWrap(True)
self.label.setStyleSheet("QLabel { font-size: 18px; }") 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 = QtWidgets.QProgressBar()
self.progress.setRange(0, 100) self.progress.setRange(0, 100)
self.progress.setValue(0) self.progress.setValue(0)
@ -493,7 +516,7 @@ class ConvertWidget(QtWidgets.QWidget):
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.dangerous_doc_label) layout.addWidget(self.dangerous_doc_label)
layout.addStretch() layout.addStretch()
layout.addWidget(self.label) layout.addLayout(label_layout)
layout.addWidget(self.progress) layout.addWidget(self.progress)
layout.addStretch() layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
@ -512,13 +535,17 @@ class ConvertWidget(QtWidgets.QWidget):
def update(self, error, text, percentage): def update(self, error, text, percentage):
if error: if error:
# TODO: add error image or something self.error = True
pass self.error_image.show()
self.progress.hide()
self.label.setText(text) self.label.setText(text)
self.progress.setValue(percentage) self.progress.setValue(percentage)
def all_done(self): def all_done(self):
if self.error:
return
# In Windows, open Explorer with the safe PDF in focus # In Windows, open Explorer with the safe PDF in focus
if platform.system() == "Windows": if platform.system() == "Windows":
dest_filename_windows = self.common.output_filename.replace("/", "\\") dest_filename_windows = self.common.output_filename.replace("/", "\\")

View file

@ -1,5 +1,4 @@
import os import os
import sys
import subprocess import subprocess
import uuid import uuid
import pipes import pipes
@ -10,6 +9,8 @@ import getpass
import json import json
import psutil import psutil
import time import time
import platform
import shutil
from PySide2 import QtCore from PySide2 import QtCore
@ -32,24 +33,8 @@ class Vm(QtCore.QObject):
self.sshd_port = None self.sshd_port = None
self.sshd_tunnel_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 # Folder to hold temporary files related to the VM
self.state_dir = tempfile.TemporaryDirectory() 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_key_path = os.path.join(self.state_dir.name, "host_ed25519")
self.ssh_host_pubkey_path = os.path.join( self.ssh_host_pubkey_path = os.path.join(
self.state_dir.name, "host_ed25519.pub" 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_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") self.vm_disk_img_path = os.path.join(self.state_dir.name, "disk.img")
# UDID for VM self.vm_iso_path = self.global_common.get_resource_path("vm/dangerzone.iso")
self.vm_uuid = str(uuid.uuid4())
self.vm_cmdline = ( if platform.system() == "Darwin":
"earlyprintk=serial console=ttyS0 modules=loop,squashfs,sd-mod" 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 # Threads
self.wait_t = None self.wait_t = None
@ -76,8 +96,7 @@ class Vm(QtCore.QObject):
self.stop() self.stop()
def start(self): def start(self):
self.state = self.STATE_STARTING print("Starting VM\n")
self.vm_state_change.emit(self.state)
# Delete keys if they already exist # Delete keys if they already exist
for filename in [ for filename in [
@ -89,10 +108,14 @@ class Vm(QtCore.QObject):
if os.path.exists(filename): if os.path.exists(filename):
os.remove(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 # Generate new keys
subprocess.run( subprocess.run(
[ [
"/usr/bin/ssh-keygen", self.ssh_keygen_path,
"-t", "-t",
"ed25519", "ed25519",
"-C", "-C",
@ -107,7 +130,7 @@ class Vm(QtCore.QObject):
) )
subprocess.run( subprocess.run(
[ [
"/usr/bin/ssh-keygen", self.ssh_keygen_path,
"-t", "-t",
"ed25519", "ed25519",
"-C", "-C",
@ -125,13 +148,9 @@ class Vm(QtCore.QObject):
with open(self.ssh_client_pubkey_path) as f: with open(self.ssh_client_pubkey_path) as f:
ssh_client_pubkey = f.read() 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 # Start an sshd service on this port
args = [ args = [
"/usr/sbin/sshd", self.sshd_path,
"-4", "-4",
"-E", "-E",
self.sshd_log_path, self.sshd_log_path,
@ -160,6 +179,16 @@ class Vm(QtCore.QObject):
print("> " + args_str) print("> " + args_str)
subprocess.run(args, stdout=self.devnull, stderr=self.devnull) 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 # Create a JSON object to pass into the VM
# This is a 512kb file that starts with a JSON object, followed by null bytes # This is a 512kb file that starts with a JSON object, followed by null bytes
guest_vm_info = { guest_vm_info = {
@ -246,6 +275,84 @@ class Vm(QtCore.QObject):
self.wait_t.timeout.connect(self.vm_timeout) self.wait_t.timeout.connect(self.vm_timeout)
self.wait_t.start() 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): def vm_connected(self):
self.state = self.STATE_ON self.state = self.STATE_ON
self.vm_state_change.emit(self.state) self.vm_state_change.emit(self.state)
@ -255,8 +362,16 @@ class Vm(QtCore.QObject):
self.vm_state_change.emit(self.state) self.vm_state_change.emit(self.state)
def stop(self): def stop(self):
# Kill existing processes
self.kill_sshd() 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: if self.vpnkit_p is not None:
self.vpnkit_p.terminate() self.vpnkit_p.terminate()
self.vpnkit_p = None self.vpnkit_p = None
@ -267,6 +382,17 @@ class Vm(QtCore.QObject):
# Just to be extra sure # Just to be extra sure
self.kill_hyperkit() 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): def find_open_port(self):
with socket.socket() as tmpsock: with socket.socket() as tmpsock:
while True: while True:

View file

@ -1,2 +0,0 @@
set DANGERZONE_MODE=container
poetry run python .\dev_scripts\dangerzone %*

View file

@ -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

View file

@ -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()

View file

@ -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

BIN
share/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB