dangerzone Documentation

Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs

5,652 stars Python Python 1 file ยท ~7,465 tokens
RAW Doc

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)
  • 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.

Licensed under the AGPLv3: https://opensource.org/licenses/agpl-3.0

text
Copyright ยฉ 2022โ€“2024 Freedom of the Press Foundation and Dangerzone contributors
Copyright ยฉ 2020โ€“2021 First Look Media

See also THIRD_PARTY_NOTICE.md for more information regarding the third-party software that Dangerzone depends on.

See also

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:

  1. 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
  2. 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.
  3. 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 -n argument,
    which is similar to make's -j argument.

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:

bash
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 everything

Finally, you can build all the release artifacts with doit, or a specific task
with:

text
doit <task>

Tips and tricks

  • You can run doit list --all -s to 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. Either podman (default)
      or docker.
    • 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:

  1. Dev environment. This environment has developer tools, necessary for
    Dangerzone, baked in. Also, it mounts the Dangerzone source under
    /home/user/dangerzone in the container. The developer can then run
    Dangerzone from source, with poetry run dangerzone.

  2. End-user environment. This environment has only Dangerzone installed in it
    from one of the following sources:

    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 with dangerzone. 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:

  1. 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, the clone syscall 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.

  2. While Linux supports nested namespaces, extra handling is required for nested
    user namespaces. By default, the configuration for each user namespace (see
    man 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 run podman in it.

  3. 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 requires CAP_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 --privileged when creating the Dangerzone environment, which
    includes the CAP_SYS_ADMIN capability.

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:

  1. 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.
  2. 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 use xhost, 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.lock and pyproject.toml. Together, these comprise the build
      context.
  • End-user environment:
    • Distro name and version. Together, these comprise the base container image.
    • .deb / .rpm Dangerzone package, as found under deb_dist/ or dist/
      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:

  1. 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.

  2. The end-user environment expects a .deb. / .rpm tagged 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.

  3. 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 (amd64 and arm64 architectures)
  • 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 (dangerzone and dangerzone-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_EPOCH is pinned to the timestamp of the latest
    debian/changelog entry. dpkg-source, dpkg-deb, and dh_builddeb use
    this value for file mtimes inside the source tarball and the binary .deb,
    and for the Date field in .changes / .dsc files. The value is set by
    debian/rules (so it's correct even when invoking dpkg-buildpackage
    directly) and re-asserted by install/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_EPOCH after dh_install
    populates the staging trees. install and pybuild do not preserve
    timestamps, so without this step the embedded data.tar would carry the
    wall-clock time of the build.
  • The build environment is pinned via the Debian Bookworm dev container,
    which fixes the versions of dpkg, 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:

bash
# 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.deb

The 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:

bash
poetry run make test

File: 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 the
settings["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 because
settings["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.

  1. Spawn a new thread so that we don't block the main window.

  2. Check if we have cached information about a release (version and changelog).
    If yes, return those immediately.

  3. 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.

  4. Hit the GitHub releases API and get the latest release.
    Store the current time as the last check time, even if the call fails.

  5. Check if the latest release matches settings["updater_latest_version"]. If
    yes, return an empty update report.

  6. If a new update has been detected, return the version number and the
    changelog.

  7. Add a green bubble in the notification icon, and a menu entry called "New
    version available".

  8. 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.

  1. Bump the number of errors we've encountered in a row
    (settings["updater_errors"] += 1)
  2. Return an update report with the error we've encountered.
  3. Update the hamburger menu with a red notification bubble, and add a menu
    entry called "Update error".
  4. 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

  1. 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.
  2. 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.
  3. 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:

  1. GitHub pages can alter the contents of our main site
    (https://dangerzone.rocks)
  2. GitHub releases can serve an older, vulnerable version of Dangerzone, instead
    of a new update.
  3. GitHub releases can serve a malicious binary (requires a joint operation from
    a malicious CA as well, for extra legitimacy).
  4. GitHub releases can silently drop updates.
  5. GitHub releases can know which users download Dangerzone updates.
  6. 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

  1. 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


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:

bash
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.