-
Notifications
You must be signed in to change notification settings - Fork 177
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
doc: Add design doc for the update notifications
Add a design document for the update notifications mechanism, adapted from the write-up in the original GitHub issue. Refs #189
- Loading branch information
Showing
1 changed file
with
222 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
# 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](https://github.com/freedomofpress/dangerzone/issues/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 | ||
|
||
This feature introduces a hamburger icon that will be visible across almost all | ||
of the Dangerzone windows. This will be used to notify the users about updates. | ||
|
||
### 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": None`: Whether to check for updates or not. `None` means | ||
that the user has not decided yet, and is the default. | ||
* `"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. | ||
|
||
Note: | ||
|
||
* If on Linux, make `"updater_check": False`, since we normally have | ||
other update channels for these platforms. | ||
|
||
### Second run | ||
|
||
_We detect it's the second time Dangerzone runs because | ||
`settings["updater_check"] is not None and settings["updater_last_check"] is | ||
None`._ | ||
|
||
Before starting up the main window, show this window: | ||
|
||
* Title: Dangerzone Updater | ||
* Body: | ||
|
||
> Do you want Dangerzone to automatically check for updates? | ||
> | ||
> If you accept, Dangerzone will check the latest releases page in github.com | ||
> on startup. Otherwise it will make no network requests and won't inform you | ||
> about new releases. | ||
> | ||
> If you prefer another way of getting notified about new releases, we suggest adding | ||
> to your RSS reader our [Mastodon feed](https://fosstodon.org/@dangerzone.rss). For more information | ||
> about updates, check [this webpage](https://github.com/freedomofpress/dangerzone/wiki/Updates). | ||
* Buttons: | ||
- Check Automaticaly: Store `settings["updater_check"] = True` | ||
- Don't Check: Store `settings["updater_check"] = False` | ||
|
||
Note: | ||
* Users will be able to change their choice from the hamburger menu, which will | ||
contain an entry called "Check for updates", that users can check and uncheck. | ||
|
||
### Subsequent runs | ||
|
||
_We perform the following only if `settings["updater_check"] == 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](https://api.github.com/repos/freedomofpress/dangerzone/releases/latest). | ||
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](https://dangerzone.rocks#downloads) 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](https://dangerzone.rocks#downloads) and check for new updates manually, or consult [this page](https://github.com/freedomofpress/dangerzone/wiki/Updates) 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](https://sparkle-project.org) 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](https://sparkle-project.org/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](https://winsparkle.org/), that follow a similar | ||
approach. I see that there's a [Python library (`pywinsparkle`)](https://pypi.org/project/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](https://github.com/sparkle-project/Sparkle/issues/345) that showcases this. | ||
|
||
## The Update Framework | ||
|
||
[The Update Framework](https://theupdateframework.io/) is a graduated CNCF | ||
project hosted by Linux Foundation. It's based on the | ||
[Thandy](https://chromium.googlesource.com/chromium/src.git/+/master/docs/updater/protocol_3_1.md) | ||
updater for Tor. It's [not widely adopted](https://github.com/sparkle-project/Sparkle/issues/345), but some of its | ||
adopters are high-profile, and it has passed security audits. | ||
|
||
It's more of a [specification](https://github.com/sparkle-project/Sparkle/issues/345) | ||
and less of a software project, although a well-maintained | ||
[reference implementation](https://github.com/sparkle-project/Sparkle/issues/345) | ||
in Python exists. Also, a [Python project (`tufup`)](https://doc.qt.io/qtinstallerframework/ifw-updates.html) | ||
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](https://theupdateframework.io/security/) 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](https://github.com/Digital-Sapphire/PyUpdater) was another popular updater project for Python, but is now archived. |