diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..86cd482 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,52 @@ +# Development environment + +After cloning this git repo, make sure to checkout the git submodules. + +``` +git submodule init +git submodule update +``` + +## Debian/Ubuntu + +You need [podman](https://podman.io/getting-started/installation) ([these instructions](https://kushaldas.in/posts/podman-on-debian-buster.html) are useful for installing in Debian or Ubuntu). + +Install dependencies: + +```sh +sudo apt install -y python3 python3-pyqt5 python3-appdirs python3-click python3-xdg +``` + +Run from source tree: + +```sh +./dev_script/dangerzone +``` + +Create a .deb: + +```sh +./install/linux/build_deb.py +``` + +## macOS + +## macOS + +Install Xcode from the Mac App Store. Once it's installed, run it for the first time to set it up. Also, run this to make sure command line tools are installed: `xcode-select --install`. And finally, open Xcode, go to Preferences > Locations, and make sure under Command Line Tools you select an installed version from the dropdown. (This is required for installing Qt5.) + +Download and install Python 3.7.4 from https://www.python.org/downloads/release/python-374/. I downloaded `python-3.7.4-macosx10.9.pkg`. + +Install Qt 5.14.0 for macOS from https://www.qt.io/offline-installers. I downloaded `qt-opensource-mac-x64-5.14.0.dmg`. In the installer, you can skip making an account, and all you need is `Qt` > `Qt 5.14.0` > `macOS`. + +If you don't have it already, install pipenv (`pip3 install --user pipenv`). Then install dependencies: + +```sh +pipenv install --dev --pre +``` + +Run from source tree: + +``` +pipenv run ./dev_scripts/dangerzone +``` diff --git a/Pipfile b/Pipfile index 3d376ac..203b5ee 100644 --- a/Pipfile +++ b/Pipfile @@ -7,10 +7,12 @@ name = "pypi" PyQt5 = "*" click = "*" appdirs = "*" -pyxdg = "*" [dev-packages] black = "*" [requires] python_version = "3.7" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..6a1598c --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,167 @@ +{ + "_meta": { + "hash": { + "sha256": "0141117b8b77eba5269d570a53da1ab9d2029bc11b6a4862111e04b8b72b9ea7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "index": "pypi", + "version": "==7.0" + }, + "pyqt5": { + "hashes": [ + "sha256:2b79209aa6e4688f6ac46e6d2694236dcf91db5f3a87270150d0f82082e3d360", + "sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b", + "sha256:3d6e315e6e2d6489a2e1e0148d00e784e277c6590c189227d6060f15b9be690a", + "sha256:812233bd155735377e2e9c7eea7a28815f357440334db51788d941e2a8b62f64", + "sha256:be10fa95e6bdc9cad616ebf368c51b3f5748138b2b3a600cf7c4f80b78cb9852" + ], + "index": "pypi", + "version": "==5.14.1" + }, + "pyqt5-sip": { + "hashes": [ + "sha256:02d94786bada670ab17a2b62ce95b3cf8e3b40c99d36007593a6334d551840bb", + "sha256:06bc66b50556fb949f14875a4c224423dbf03f972497ccb883fb19b7b7c3b346", + "sha256:091fbbe10a7aebadc0e8897a9449cda08d3c3f663460d812eca3001ca1ed3526", + "sha256:0a067ade558befe4d46335b13d8b602b5044363bfd601419b556d4ec659bca18", + "sha256:1910c1cb5a388d4e59ebb2895d7015f360f3f6eeb1700e7e33e866c53137eb9e", + "sha256:1c7ad791ec86247f35243bbbdd29cd59989afbe0ab678e0a41211f4407f21dd8", + "sha256:3c330ff1f70b3eaa6f63dce9274df996dffea82ad9726aa8e3d6cbe38e986b2f", + "sha256:482a910fa73ee0e36c258d7646ef38f8061774bbc1765a7da68c65056b573341", + "sha256:7695dfafb4f5549ce1290ae643d6508dfc2646a9003c989218be3ce42a1aa422", + "sha256:8274ed50f4ffbe91d0f4cc5454394631edfecd75dc327aa01be8bc5818a57e88", + "sha256:9047d887d97663790d811ac4e0d2e895f1bf2ecac4041691487de40c30239480", + "sha256:9f6ab1417ecfa6c1ce6ce941e0cebc03e3ec9cd9925058043229a5f003ae5e40", + "sha256:b43ba2f18999d41c3df72f590348152e14cd4f6dcea2058c734d688dfb1ec61f", + "sha256:c3ab9ea1bc3f4ce8c57ebc66fb25cd044ef92ed1ca2afa3729854ecc59658905", + "sha256:da69ba17f6ece9a85617743cb19de689f2d63025bf8001e2facee2ec9bcff18f", + "sha256:ef3c7a0bf78674b0dda86ff5809d8495019903a096c128e1f160984b37848f73", + "sha256:fabff832046643cdb93920ddaa8f77344df90768930fbe6bb33d211c4dcd0b5e" + ], + "version": "==12.7.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "black": { + "hashes": [ + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + ], + "index": "pypi", + "version": "==19.10b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "index": "pypi", + "version": "==7.0" + }, + "pathspec": { + "hashes": [ + "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", + "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + ], + "version": "==0.7.0" + }, + "regex": { + "hashes": [ + "sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525", + "sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b", + "sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576", + "sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5", + "sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0", + "sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35", + "sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003", + "sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d", + "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161", + "sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26", + "sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9", + "sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1", + "sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146", + "sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f", + "sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149", + "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351", + "sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461", + "sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b", + "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242", + "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c", + "sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77" + ], + "version": "==2020.1.8" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "typed-ast": { + "hashes": [ + "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", + "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", + "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", + "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", + "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", + "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", + "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", + "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", + "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", + "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", + "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", + "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", + "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", + "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", + "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", + "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", + "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + ], + "version": "==1.4.0" + } + } +} diff --git a/README.md b/README.md index 7facab0..c28aff8 100644 --- a/README.md +++ b/README.md @@ -15,36 +15,6 @@ Some features: - Dangerzone compresses the safe PDF to reduce file size - After converting, dangerzone lets you open the safe PDF in the PDF viewer of your choice, which allows you to open PDFs and office docs in dangerzone by default so you never accidentally open a dangerous document -Dangerzone was inspired by [Qubes trusted PDF](https://blog.invisiblethings.org/2013/02/21/converting-untrusted-pdfs-into-trusted.html), but it works in non-Qubes operating systems and sandboxes the document conversion in [podman](https://podman.io/) containers instead of virtual machines. Podman is like docker but more secure -- it doesn't require a privileged daemon, and containers can be launched without root. +Dangerzone was inspired by [Qubes trusted PDF](https://blog.invisiblethings.org/2013/02/21/converting-untrusted-pdfs-into-trusted.html), but it works in non-Qubes operating systems and sandboxes the document conversion in containers instead of virtual machines (using [podman](https://podman.io/) for Linux, and Docker for macOS, for now). Podman is like docker but more secure -- it doesn't require a privileged daemon, and containers can be launched without root. -Right now, dangerzone only works in Linux, but the goal is to [get it working in macOS](https://github.com/firstlookmedia/dangerzone/issues/1) so it can be more useful to journalists (who tend to <3 using Macs). - -## Development environment - -After cloning this git repo, make sure to checkout the git submodules. - -``` -git submodule init -``` - -### Debian/Ubuntu - -You need [podman](https://podman.io/getting-started/installation) ([these instructions](https://kushaldas.in/posts/podman-on-debian-buster.html) are useful for installing in Debian or Ubuntu). - -Install dependencies: - -``` -sudo apt install -y python3 python3-pyqt5 python3-appdirs python3-click python3-xdg -``` - -Run locally: - -``` -./dev_script/dangerzone -``` - -Create a .deb: - -``` -./install/linux/build_deb.py -``` +Set up a development environment by following [these instructions](/BUILD.md). diff --git a/dangerzone/common.py b/dangerzone/common.py index 208d150..5027fc3 100644 --- a/dangerzone/common.py +++ b/dangerzone/common.py @@ -3,8 +3,11 @@ import os import inspect import tempfile import appdirs +import platform from PyQt5 import QtGui -from xdg.DesktopEntry import DesktopEntry + +if platform.system() == "Linux": + from xdg.DesktopEntry import DesktopEntry from .settings import Settings @@ -19,8 +22,9 @@ class Common(object): self.app = app # Temporary directory to store pixel data - self.pixel_dir = tempfile.TemporaryDirectory() - self.safe_dir = tempfile.TemporaryDirectory() + # Note in macOS, temp dirs must be in /tmp (or a few other paths) for Docker to mount them + self.pixel_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-pixel-") + self.safe_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-safe-") print( f"Temporary directories created, dangerous={self.pixel_dir.name}, safe={self.safe_dir.name}" ) @@ -37,6 +41,12 @@ class Common(object): # App data folder self.appdata_path = appdirs.user_config_dir("dangerzone") + # Container runtime + if platform.system() == "Darwin": + self.container_runtime = "docker" + else: + self.container_runtime = "podman" + # Preload list of PDF viewers on computer self.pdf_viewers = self._find_pdf_viewers() @@ -231,26 +241,27 @@ class Common(object): def _find_pdf_viewers(self): pdf_viewers = {} - for search_path in [ - "/usr/share/applications", - "/usr/local/share/applications", - os.path.expanduser("~/.local/share/applications"), - ]: - try: - for filename in os.listdir(search_path): - full_filename = os.path.join(search_path, filename) - if os.path.splitext(filename)[1] == ".desktop": + if platform.system == "Linux": + for search_path in [ + "/usr/share/applications", + "/usr/local/share/applications", + os.path.expanduser("~/.local/share/applications"), + ]: + try: + for filename in os.listdir(search_path): + full_filename = os.path.join(search_path, filename) + if os.path.splitext(filename)[1] == ".desktop": - desktop_entry = DesktopEntry(full_filename) - if ( - "application/pdf" in desktop_entry.getMimeTypes() - and desktop_entry.getName() != "dangerzone" - ): - pdf_viewers[ - desktop_entry.getName() - ] = desktop_entry.getExec() + desktop_entry = DesktopEntry(full_filename) + if ( + "application/pdf" in desktop_entry.getMimeTypes() + and desktop_entry.getName() != "dangerzone" + ): + pdf_viewers[ + desktop_entry.getName() + ] = desktop_entry.getExec() - except FileNotFoundError: - pass + except FileNotFoundError: + pass return pdf_viewers diff --git a/dangerzone/settings.py b/dangerzone/settings.py index 4814f53..3e0d0a7 100644 --- a/dangerzone/settings.py +++ b/dangerzone/settings.py @@ -8,12 +8,17 @@ class Settings: self.common = common self.settings_filename = os.path.join(self.common.appdata_path, "settings.json") + if len(self.common.pdf_viewers) == 0: + default_pdf_viewer = None + else: + default_pdf_viewer = list(self.common.pdf_viewers)[0] + self.default_settings = { "save": True, "ocr": True, "ocr_language": "English", "open": True, - "open_app": list(self.common.pdf_viewers)[0], + "open_app": default_pdf_viewer, "update_container": True, } diff --git a/dangerzone/settings_widget.py b/dangerzone/settings_widget.py index 84a56ad..1a7ee03 100644 --- a/dangerzone/settings_widget.py +++ b/dangerzone/settings_widget.py @@ -111,7 +111,9 @@ class SettingsWidget(QtWidgets.QWidget): self.update_checkbox.setCheckState(QtCore.Qt.Unchecked) # Is update containers required? - output = subprocess.check_output(["podman", "image", "ls", "dangerzone"]) + output = subprocess.check_output( + [self.common.container_runtime, "image", "ls", "dangerzone"] + ) if b"localhost/dangerzone" not in output: self.update_checkbox.setCheckState(QtCore.Qt.Checked) self.update_checkbox.setEnabled(False) diff --git a/dangerzone/tasks.py b/dangerzone/tasks.py index f2146f7..cc5a464 100644 --- a/dangerzone/tasks.py +++ b/dangerzone/tasks.py @@ -3,6 +3,7 @@ import time import tempfile import os import pipes +import platform from PyQt5 import QtCore, QtWidgets, QtGui @@ -15,7 +16,8 @@ class TaskBase(QtCore.QThread): def __init__(self): super(TaskBase, self).__init__() - def execute_podman(self, args, watch="stdout"): + def exec_container(self, args, watch="stdout"): + args = [self.common.container_runtime] + args args_str = " ".join(pipes.quote(s) for s in args) print(f"Executing: {args_str}") output = f"Executing: {args_str}\n\n" @@ -55,8 +57,8 @@ class PullImageTask(TaskBase): def run(self): self.update_label.emit("Pulling container image") self.update_details.emit("") - args = ["podman", "pull", "ubuntu:18.04"] - self.execute_podman(args, watch="stderr") + args = ["pull", "ubuntu:18.04"] + self.exec_container(args, watch="stderr") self.task_finished.emit() @@ -69,8 +71,8 @@ class BuildContainerTask(TaskBase): container_path = self.common.get_resource_path("container") self.update_label.emit("Building container") self.update_details.emit("") - args = ["podman", "build", "-t", "dangerzone", container_path] - self.execute_podman(args) + args = ["build", "-t", "dangerzone", container_path] + self.exec_container(args) self.task_finished.emit() @@ -86,7 +88,6 @@ class ConvertToPixels(TaskBase): def run(self): self.update_label.emit("Converting document to pixels") args = [ - "podman", "run", "--network", "none", @@ -97,7 +98,7 @@ class ConvertToPixels(TaskBase): "dangerzone", "document-to-pixels", ] - output = self.execute_podman(args) + output = self.exec_container(args) # Did we hit an error? for line in output.split("\n"): @@ -185,7 +186,6 @@ class ConvertToPDF(TaskBase): args = ( [ - "podman", "run", "--network", "none", @@ -197,5 +197,5 @@ class ConvertToPDF(TaskBase): + envs + ["dangerzone", "pixels-to-pdf",] ) - self.execute_podman(args) + self.exec_container(args) self.task_finished.emit()