File: README.md
Dangerzone
Take potentially dangerous PDFs, office documents, or images and convert them to a safe PDF.
| |
|--|--|
Dangerzone works like this: You give it a document that you don't know if you can trust (for example, an email attachment). Inside of a sandbox, Dangerzone converts the document to a PDF (if it isn't already one), and then converts the PDF into raw pixel data: a huge list of RGB color values for each page. Then, outside of the sandbox, Dangerzone takes this pixel data and converts it back into a PDF.
Read more about Dangerzone in the official site.
Getting started
Follow the instructions for each platform:
You can read more about our operating system support here.
Some features
- Sandboxes don't have network access, so if a malicious document can compromise one, it can't phone home
- Sandboxes use gVisor, an application kernel written in Go, that implements a substantial portion of the Linux system call interface.
- Dangerzone can optionally OCR the safe PDFs it creates, so it will have a text layer again
- 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 can convert these types of document into safe PDFs:
- PDF (
.pdf) - Microsoft Word (
.docx,.doc) - Microsoft Excel (
.xlsx,.xls) - Microsoft PowerPoint (
.pptx,.ppt) - ODF Text (
.odt) - ODF Spreadsheet (
.ods) - ODF Presentation (
.odp) - ODF Graphics (
.odg) - Hancom HWP (Hangul Word Processor) (
.hwp,.hwpx)- Not supported on
Qubes OS
- Not supported on
- EPUB (
.epub) - Jpeg (
.jpg,.jpeg) - GIF (
.gif) - PNG (
.png) - SVG (
.svg) - other image formats (
.bmp,.pnm,.pbm,.ppm,.tif,.tiff)
Dangerzone was inspired by Qubes trusted PDF, but it works in non-Qubes operating systems. It uses Podman containers as sandboxes instead of virtual machines.
Set up a development environment by following these instructions.
License and Copyright
Licensed under the AGPLv3: https://opensource.org/licenses/agpl-3.0
Copyright ยฉ 2022โ2024 Freedom of the Press Foundation and Dangerzone contributors
Copyright ยฉ 2020โ2021 First Look MediaSee also THIRD_PARTY_NOTICE.md for more information regarding the third-party software that Dangerzone depends on.
See also
- GIJN Toolbox: Cutting-Edge โ and Free โ Online Investigative Tools You Can Try Right Now
- When security matters: working with Qubes OS at the Guardian
FAQ
Has Dangerzone received a security audit?
Yes, Dangerzone received its first security audit by Include Security in December 2023. The audit was generally favorable, as it didn't identify any high-risk findings, except for 3 low-risk and 7 informational findings.
"I'm experiencing an issue while using Dangerzone."
Dangerzone gets updates to improve its features and to fix problems. So, updating may be the simplest path to resolving the issue which brought you here. Here is how to update:
- Check which version of Dangerzone you are currently using: run Dangerzone, then look for a series of numbers to the right of the logo within the app. The format of the numbers will look similar to
0.4.1 - Now find the latest available version of Dangerzone: go to the download page. Look for the version number displayed. The number will be using the same format as in Step 1.
- Is the version on the Dangerzone download page higher than the version of your installed app? Go ahead and update.
Can I run Dangerzone in an airgapped environment?
Yes, Dangerzone is designed to run in airgapped environments without any
configuration. If you want to update its container image, follow
our instructions.
Can I use a custom runtime, such as Podman Desktop?
On Windows and macOS, Dangerzone embeds Podman, so there is no need to.
To use a different podman version, such as Podman Desktop, follow our documentation.
File: dev_scripts/README.md
Developer scripts
This directory holds some scripts that are helpful for developing on Dangerzone.
Read the respective documentation for more details on some of the scripts.
File: docs/developer/doit.md
Using the Doit Automation Tool
Developers can use the Doit automation tool to create
release artifacts. The purpose of the tool is to automate the manual release
instructions in RELEASE.md file. Not everything is automated yet, since we're
still experimenting with this tool. You can find our task definitions in this
repo's dodo.py file.
Why Doit?
We picked Doit out of the various tools out there for the following reasons:
- Pythonic: The configuration file and tasks can be written in Python. Where
applicable, it's easy to issue shell commands as well. - File targets: Doit borrows the file target concept from Makefiles. Tasks
can have file dependencies, and targets they build. This makes it easy to
define a dependency graph for tasks. - Hash-based caching: Unlike Makefiles, doit does not look at the
modification timestamp of source/target files, to figure out if it needs to
run them. Instead, it hashes those files, and will run a task only if the
hash of a file dependency has changed. - Parallelization: Tasks can be run in parallel with the
-nargument,
which is similar tomake's-jargument.
How to Doit?
First, enter your Poetry shell. Then, make sure that your environment is clean,
and you have ample disk space. You can run:
doit clean --dry-run # if you want to see what would happen
doit clean # you'll be asked to confirm that you want to clean everythingFinally, you can build all the release artifacts with doit, or a specific task
with:
doit <task>Tips and tricks
- You can run
doit list --all -sto see the full list of tasks, their
dependencies, and whether they are up to date (U) or will run (R). Note that
certain small tasks are always configured to run. - You can run
doit info <task>to see which dependencies are missing. - You can pass the following environment variables to the script, in order to
affect some global parameters:CONTAINER_RUNTIME: The container runtime to use. Eitherpodman(default)
ordocker.RELEASE_DIR: Where to store the release artifacts. Default path is~/release-assets/<version>APPLE_ID: The Apple ID to use when signing/notarizing the macOS DMG.
File: docs/developer/environments.md
Create Dangerzone environments
The dev_scripts/env.py script creates environments where a user can run
Dangerzone, allows the user to run arbitrary commands in these environments, as
well as run Dangerzone (nested containerization).
It supports two types of environments:
Dev environment. This environment has developer tools, necessary for
Dangerzone, baked in. Also, it mounts the Dangerzone source under/home/user/dangerzonein the container. The developer can then run
Dangerzone from source, withpoetry run dangerzone.End-user environment. This environment has only Dangerzone installed in it
from one of the following sources:- (default): from the .deb/.rpm package that we have created
--qa: from https://packages-qa.freedom.press--prod: from https://packages.freedom.press
For convenience, it also has the Dangerzone source mounted under
/home/user/dangerzone, but it lacks Poetry and other build tools. The
developer can run Dangerzone there withdangerzone. This environment is the
most vanilla Dangerzone environment, and should be closer to the end user's
environment, than the development environment.
Each environment corresponds to a Dockerfile, which is generated on the fly. The
developer can see this Dockerfile by passing --show-dockerfile.
For usage information, run ./dev_scripts/env.py --help.
Nested containerization
As Dangerzone environments are containers, the isolation sandbox that Dangerzone create is a nested container.
This brings the following challenges:
Containers typically only have a subset of syscalls allowed, and sometimes
only for specific arguments. This happens with the use of
seccomp filters. For
instance, in Docker, theclonesyscall is limited in containers and cannot
create new namespaces
(https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile). For testing/development purposes, a seccomp filter for the external container is used to get around this limitation, with--security-opt seccomp=unconfined. This has the same effect as developing
Dangerzone locally, so it should probably be sufficient for now.While Linux supports nested namespaces, extra handling is required for nested
user namespaces. By default, the configuration for each user namespace (seeman login.defs
is to reserve 65536 UIDs/GIDs, starting from UID/GID 100000. This works fine
for the first container, but can't work for the nested container, since it
doesn't have enough UIDs/GIDs to refer to UID 100000. Our solution to this is
to restrict the number of UIDs/GIDs allowed in the nested container to 2000,
which should be enough to runpodmanin it.Containers also restrict the capabilities (see
man capabilities)
of the processes that run in them. By default, containers do not have mount
capabilities, since it requiresCAP_SYS_ADMIN, which effectively
makes the process root in the specific
user namespace. In our case, we have to give the Dangerzone environment this
capability, since it will have to mount directories in Podman containers. For
this reason, as well as some extra things we bumped into during development,
we pass--privilegedwhen creating the Dangerzone environment, which
includes theCAP_SYS_ADMINcapability.
GUI containerization
Running a GUI app in a container is a tricky subject for multi-platform apps. In
our case, we deal specifically with Linux environments, so we can target just
this platform.
To understand how a GUI app can draw in the user's screen from within a
container, we must first understand how it does so outside the container. In
Unix-like systems, GUI apps act like
clients to a display server.
The most common display server implementation is X11, and the runner-up is
Wayland. Both of these display servers share some common traits, mainly that
they use Unix domain sockets as a way of letting clients communicate with them.
So, this gives us the answer on how one can run a containerized GUI app; they
can simply mount the Unix Domain Socket in the container. In practice this is
more nuanced, for two reasons:
- Wayland support is not that mature on Linux, so we need to
set some extra environment variables. To simplify things, we will target
X11 / XWayland hosts, which are the majority of the Linux OSes out there. - Sharing the Unix Domain socket does not allow the client to talk to the
display server, for security reasons. In order to allow the client, we need
to mount a magic cookie stored in a file pointed at by the$XAUTHORITY
envvar. Else, we can usexhost, which is considered slightly more dangerous
for multi-user environments.
Caching and Reproducibility
In order to build Dangerzone environments, the script uses the following inputs:
- Dev environment:
- Distro name and version. Together, these comprise the base container image.
poetry.lockandpyproject.toml. Together, these comprise the build
context.
- End-user environment:
- Distro name and version. Together, these comprise the base container image.
.deb/.rpmDangerzone package, as found underdeb_dist/ordist/
respectively.
Any change in these inputs busts the cache for the corresponding image. In
theory, this means that the Dangerzone environment for each commit can be built
reproducibly. In practice, there are some issues that we haven't covered yet:
The output images are:
- Dev:
dangerzone.rocks/build/{distro_name}:{distro_version} - End-user:
dangerzone.rocks/{distro_name}:{distro_version}
These images do not contain the commit/version of the Dangerzone source they
got created from, so each one overrides the other.- Dev:
The end-user environment expects a
.deb./.rpmtagged with the version
of Dangerzone, but it doesn't insist being built from the current Dangerzone
commit. This means that stale packages may be installed in the end-user
environment.The base images may be different in various environments, depending on when
they where pulled.
State
The main goal behind these Dangerzone environments is to make them immutable,
so that they do not require to be stored somewhere, but can be recreated from
their images. Any change to these environments should therefore be reflected to
their Dockerfile.
To enforce immutability, we delete the containers every time we run a command or
an interactive shell exits. This means that these environments are suitable only
for running Dangerzone commands, and not doing actual development in them
(install an editor, configure bash prompts, etc.)
The only point where we allow mutability is the directory where Podman stores
the images and stopped containers, which may be useful for developers. If this
proves to be an issue, we will reconsider.
File: docs/developer/reproducibility.md
Reproducible builds
We want to improve the transparency and auditability of our build artifacts, and
a way to achieve this is via reproducible builds. For a broader understanding of
what reproducible builds entail, check out https://reproducible-builds.org/.
Our build artifacts consist of:
- Container images (
amd64andarm64architectures) - macOS installers (for Intel and Apple Silicon CPUs)
- Windows installer
- Fedora packages (for regular Fedora distros and Qubes)
- Debian packages (for Debian and Ubuntu)
As of writing this, the following artifacts are reproducible:
- Container images (see #1047). You can find a detailed documentation on reproducible containers in the dangerzone-image repository.
- Debian packages (
dangerzoneanddangerzone-full).
Debian packages
Building the .deb files inside our pinned Debian Bookworm dev container
(./dev_scripts/env.py --distro debian --version bookworm) and via./install/linux/build-deb.py produces byte-identical packages on every run,
given the same source tree and the same share/container.tar.
The mechanisms that make this work:
SOURCE_DATE_EPOCHis pinned to the timestamp of the latestdebian/changelogentry.dpkg-source,dpkg-deb, anddh_builddebuse
this value for file mtimes inside the source tarball and the binary.deb,
and for theDatefield in.changes/.dscfiles. The value is set bydebian/rules(so it's correct even when invokingdpkg-buildpackage
directly) and re-asserted byinstall/linux/build-deb.py.- Locale and timezone are forced to
C.UTF-8/UTC, so any tool that
emits sorted output or formatted timestamps does so identically across
machines. - File mtimes are clamped to
SOURCE_DATE_EPOCHafterdh_install
populates the staging trees.installand pybuild do not preserve
timestamps, so without this step the embeddeddata.tarwould carry the
wall-clock time of the build. - The build environment is pinned via the Debian Bookworm dev container,
which fixes the versions ofdpkg,debhelper,pybuild, and the Python
interpreter that participate in the build.
Verifying reproducibility
To confirm a .deb is reproducible, build it twice in clean environments and
compare the outputs:
# First build
./dev_scripts/env.py --distro debian --version bookworm run --dev bash -c \
"cd dangerzone && ./install/linux/build-deb.py"
mv deb_dist deb_dist.first
# Clean and build again
git clean -fdx debian/ deb_dist/
./dev_scripts/env.py --distro debian --version bookworm run --dev bash -c \
"cd dangerzone && ./install/linux/build-deb.py"
# Compare
sha256sum deb_dist.first/*.deb deb_dist/*.deb
# For a deeper diff if hashes differ:
# diffoscope deb_dist.first/dangerzone_<ver>_amd64.deb deb_dist/dangerzone_<ver>_amd64.debThe two sets of .deb files should have matching SHA-256 sums. If they don't,diffoscope will show what diverged โ common culprits are unpinned timestamps,
locale-dependent sort order, or files generated outside the staging trees.
File: docs/developer/TESTING.md
Dangerzone Testing
Dangerzone has some automated testing under tests/.
The following assumes that you have already setup the development environment.
Run tests
Unit / integration tests are run with:
poetry run make testFile: docs/developer/updates.md
Update notifications
This design document explains how the notification mechanism for Dangerzone
updates works, what are its benefits and limitations, and what other
alternatives we have considered. It has been adapted by discussions on GitHub
issue #189, and has
been updated to reflect the current design.
A user-facing document on how update notifications work can be found in
https://github.com/freedomofpress/dangerzone/wiki/Updates
Design overview
A hamburger icon is visible across almost all of the Dangerzone windows, and is used to notify the users when there are new releases.
First run
We detect it's the first time Dangerzone runs because thesettings["updater_last_check"] is None.
Add the following keys in our settings.json file.
"updater_check_all": True: Whether or not to check and apply independent container updates and check for new releases."updater_last_check": None: The last time we checked for updates (in seconds
from Unix epoch). None means that we haven't checked yet."updater_latest_version": "0.4.2": The latest version that the Dangerzone
updater has detected. By default it's the current version."updater_latest_changelog": "": The latest changelog that the Dangerzone
updater has detected. By default it's empty."updater_errors: 0: The number of update check errors that we have
encountered in a row.
Previously, "updater_check" was used to determine if we should check for new releases, and has been replaced by "updater_check_all" when adding support for independent container updates.
Second run
We detect it's the second time Dangerzone runs becausesettings["updater_check_all"] is not None and settings["updater_last_check"] is None.
Before starting up the main window, the user is prompted if they want to enable update checks.
Subsequent runs
We perform the following only if settings["updater_check_all"] == True.
Spawn a new thread so that we don't block the main window.
Check if we have cached information about a release (version and changelog).
If yes, return those immediately.Check if the last time we checked for new releases was less than 12 hours
ago. In that case, skip this update check so that we don't leak telemetry
stats to GitHub.Hit the GitHub releases API and get the latest release.
Store the current time as the last check time, even if the call fails.Check if the latest release matches
settings["updater_latest_version"]. If
yes, return an empty update report.If a new update has been detected, return the version number and the
changelog.Add a green bubble in the notification icon, and a menu entry called "New
version available".Users who click on this entry will see a dialog with more info:
Title: "Dangerzone v0.5.0 has been released"
Body:
A new Dangerzone version been released. Please visit our downloads page to install this update.
(Show changelog rendered from Markdown in a collapsible text box)
Buttons:
- OK: Return
Notes:
- Any successful attempt to fetch info from GitHub will result in clearing the
settings["updater_errors"]key.
Error handling
We trigger error handling when the updater thread encounters an error (either
due to an HTTPS failure or a Python exception) and does not complete
successfully.
- Bump the number of errors we've encountered in a row
(settings["updater_errors"] += 1) - Return an update report with the error we've encountered.
- Update the hamburger menu with a red notification bubble, and add a menu
entry called "Update error". - If a user clicks on this menu entry, show a dialog window:
Title: "Update check error"
Body:
Something went wrong while checking for Dangerzone updates:
You are strongly advised to visit our downloads page and check for new updates manually, or consult this page for common causes of errors . Alternatively, you can uncheck "Check for updates", if you are in an air-gapped environment and have another way of learning about updates.
(Show the latest error message in a scrollable, copyable text box)
Buttons:
- Close: Return
Key Benefits
- The above approach future-proofs Dangerzone against API changes or bugs in
the update check process, by asking users to manually visit
https://dangerzone.rocks. - If we want to draw the attention of users to immediately install a release,
we can do so in the release body, which we will show in a pop-up window. - If we are aware of issues that prevent updates, we can add them in the wiki
page that we show in the error popup. Wiki pages are not versioned, so we can
add useful info even after a release.
Security Considerations
Because this approach does not download binaries / auto-updates, it does not
add any more security issues than the existing, manual way of installing
updates. These issues have to do with a compromised/malicous GitHub service, and
are the following:
- GitHub pages can alter the contents of our main site
(https://dangerzone.rocks) - GitHub releases can serve an older, vulnerable version of Dangerzone, instead
of a new update. - GitHub releases can serve a malicious binary (requires a joint operation from
a malicious CA as well, for extra legitimacy). - GitHub releases can silently drop updates.
- GitHub releases can know which users download Dangerzone updates.
- Network attackers can know that a user has Dangerzone installed (because we ask the user to visit https://dangerzone.rocks)
A good update framework would probably defend against 1,2,3. This is not to say
that our users are currently unprotected, since 1-4 can be detected by the
general public and the developers (unless GitHub specifically targets an
individual, but that's another story).
Usability Considerations
- We do not have an update story for users that only use the Dangerzone CLI. A
good assumption is that they are on Linux, so they auto-update.
Alternatives
We researched a bit on this subject and found out that there are update
frameworks that do this job for us. While working on this issue, we decided that
integrating with one framework will certainly take a bit of work, especially
given that we target both Windows and MacOS systems. In the meantime though, we
didn't want to have releases out without including at least a notification
channel, since staying behind on updates has a huge negative impact on the
users' safety.
The update frameworks that we learned about are:
Sparkle Project
Sparkle project seems to be the de-facto update
framework in MacOS. Integrators in practice need to care about two things:
creating a proper Appcast.xml file on the server-side, and calling the Sparkle
code from the client-side. These are covered in the project's
documentation.
The client-side part is not very straight-forward, since Sparkle is written in
Objective-C. Thankfully, there are others who have ventured into this before:
https://fman.io/blog/codesigning-and-automatic-updates-for-pyqt-apps/
The server-side part is also not very straight-forward. For integrators that use
GitHub releases (like us), this issue may be of help:
https://github.com/sparkle-project/Sparkle/issues/648
The Windows platform is not covered by Sparkle itself, but there are other
projects, such as WinSparkle, that follow a similar
approach. I see that there's a Python library (pywinsparkle)
for interacting with WinSparkle, so this may alleviate some pains.
Note that the Sparkle project is not a silver bullet. Development missteps can
happen, and users can be left without updates. Here's an example issue that showcases this.
The Update Framework
The Update Framework is a graduated CNCF
project hosted by Linux Foundation. It's based on the
Thandy
updater for Tor. It's not widely adopted, but some of its
adopters are high-profile, and it has passed security audits.
It's more of a specification
and less of a software project, although a well-maintained
reference implementation
in Python exists. Also, a Python project (tufup)
that builds upon this implementation makes it even easier to generate the
required keys and files.
Regardless of whether we use it, knowing about the threat vectors that it's protecting against is very important.
Other Projects
- Qt has some updater framework as well: https://doc.qt.io/qtinstallerframework/ifw-updates.html
- Google Chrome has it's own updater framework: https://chromium.googlesource.com/chromium/src.git/+/master/docs/updater/protocol_3_1.md
- Keepass rolls out its own way to update: https://github.com/keepassxreboot/keepassxc/blob/develop/src/updatecheck/UpdateChecker.cpp
- PyUpdater was another popular updater project for Python, but is now archived.
File: docs/advisories/2023-10-25.md
Security Advisory 2023-10-25
For users testing our new Qubes integration (beta), please note that our instructions were missing a configuration detail for disposable VMs which is necessary to fully harden the configuration.
These instructions apply to users who followed the setup instructions before October 25, 2023.
What you need to do: run the following command in dom0:
qvm-prefs dz-dvm default_dispvm ''Explanation: In Qubes OS, the default template for disposable VMs is network-connected. For this reason, we instruct users to create their own disposable VM (dz-dvm). However, adversaries with the ability to execute commands on dz-dvm would also be able open new disposable VMs with the default settings. By setting the default_dispvm to "none" we prevent this bypass.