├── .eslintrc.yml ├── .gitignore ├── .gitlab-ci.yml ├── .pylintrc ├── .qubesbuilder ├── LICENSE-0BSD ├── Makefile.builder ├── README.md ├── archlinux └── PKGBUILD.in ├── debian ├── changelog ├── control ├── copyright ├── docs ├── patches │ ├── esr.diff │ └── series ├── rules └── source │ └── format ├── rpm_spec ├── lib64.diff └── qubes-split-browser.spec.in ├── scripts ├── run-eslint ├── run-pylint └── run-shellcheck ├── version └── vm ├── GNUmakefile ├── qubes-split-browser-disp ├── etc │ ├── qubes-rpc │ │ └── split-browser-disp │ └── split-browser-disp │ │ ├── 21-tor-browser.bash │ │ ├── 22-firefox.bash.EXAMPLE │ │ └── 23-mullvad-browser.bash.EXAMPLE └── usr │ ├── lib │ └── tmpfiles.d │ │ └── split-browser-disp.conf │ └── share │ └── split-browser-disp │ └── firefox │ ├── sb-load.js │ └── sb.js └── qubes-split-browser ├── etc └── split-browser │ ├── 10-defaults.bash │ ├── 20-utf-8.bash.EXAMPLE │ └── prefs │ ├── 11-no-updates.js │ ├── 12-no-tor-checks.js │ ├── 14-no-nags.js │ ├── 15-downloads-directory.js │ └── 32-ui-tweaks.js.EXAMPLE └── usr ├── bin ├── split-browser ├── split-browser-bookmark ├── split-browser-bookmark-pretty ├── split-browser-bookmark-pretty2url ├── split-browser-cmd ├── split-browser-login ├── split-browser-login-fields └── x11-unoverride-redirect ├── lib └── tmpfiles.d │ └── split-browser.conf └── share └── applications ├── split-browser-safest.desktop └── split-browser.desktop /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: eslint:recommended 2 | parserOptions: 3 | ecmaVersion: 2019 4 | globals: 5 | Components: readonly 6 | Cc: readonly 7 | Ci: readonly 8 | ChromeUtils: readonly 9 | pref: readonly 10 | rules: 11 | no-empty-pattern: 12 | - off 13 | semi: 14 | - error 15 | - always 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debian/*.debhelper 2 | debian/*.substvars 3 | debian/debhelper-build-stamp 4 | debian/files 5 | debian/qubes-split-browser/ 6 | debian/qubes-split-browser-disp/ 7 | pkgs/ 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: 'QubesOS/qubes-continuous-integration' 3 | file: '/r4.1/gitlab-base.yml' 4 | - project: 'QubesOS/qubes-continuous-integration' 5 | file: '/r4.1/gitlab-vm.yml' 6 | - project: 'QubesOS/qubes-continuous-integration' 7 | file: '/r4.2/gitlab-base.yml' 8 | - project: 'QubesOS/qubes-continuous-integration' 9 | file: '/r4.2/gitlab-vm.yml' 10 | 11 | variables: 12 | qb_opts: '-o +components+app-split-browser' 13 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=invalid-name,missing-docstring 3 | -------------------------------------------------------------------------------- /.qubesbuilder: -------------------------------------------------------------------------------- 1 | vm: 2 | rpm: 3 | build: 4 | - rpm_spec/qubes-split-browser.spec 5 | deb: 6 | build: 7 | - debian 8 | archlinux: 9 | build: 10 | - archlinux 11 | -------------------------------------------------------------------------------- /LICENSE-0BSD: -------------------------------------------------------------------------------- 1 | Copyright 2016-2025 Rusty Bird 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /Makefile.builder: -------------------------------------------------------------------------------- 1 | ifeq ($(PACKAGE_SET),vm) 2 | 3 | RPM_SPEC_FILES = rpm_spec/qubes-split-browser.spec 4 | DEBIAN_BUILD_DIRS = debian 5 | ARCH_BUILD_DIRS = archlinux 6 | 7 | # Support for new packaging 8 | ifneq ($(filter $(DISTRIBUTION), archlinux),) 9 | VERSION := $(file <$(ORIG_SRC)/$(DIST_SRC)/version) 10 | GIT_TARBALL_NAME ?= qubes-split-browser-$(VERSION)-1.tar.gz 11 | SOURCE_COPY_IN := source-archlinux-copy-in 12 | 13 | source-archlinux-copy-in: PKGBUILD = $(CHROOT_DIR)/$(DIST_SRC)/$(ARCH_BUILD_DIRS)/PKGBUILD 14 | source-archlinux-copy-in: 15 | cp $(PKGBUILD).in $(CHROOT_DIR)/$(DIST_SRC)/PKGBUILD 16 | sed -i "s/@VERSION@/$(VERSION)/g" $(CHROOT_DIR)/$(DIST_SRC)/PKGBUILD 17 | sed -i "s/@REL@/1/g" $(CHROOT_DIR)/$(DIST_SRC)/PKGBUILD 18 | endif 19 | 20 | endif 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Split Browser for Qubes OS 2 | 3 | Everyone loves the [Whonix approach](https://www.whonix.org/wiki/Qubes) of running Tor Browser and the tor daemon in two separate [Qubes OS](https://www.qubes-os.org/) qubes (virtual machines), e.g. anon-whonix and sys-whonix. 4 | 5 | Let's take it a step further and **run Tor Browser (or Firefox) in a [disposable](https://www.qubes-os.org/doc/how-to-use-disposables/) connecting through the tor qube (or another network-providing qube), while storing bookmarks and logins in a persistent qube** - with carefully restricted data flow. 6 | 7 | In this setup, the disposable's browser can send various requests to the persistent qube: 8 | 9 | - Bookmark the current page 10 | - Let the user choose a bookmark to load 11 | - Let the user authorize logging into the current page 12 | 13 | **But if the browser gets exploited, it won't be able to read all your bookmarks or login credentials and send them to the attacker.** And you can restart the disposable frequently (which should only take a few seconds) to "shake off" such an attack. 14 | 15 | 16 | ## Keyboard shortcuts 17 | 18 | The bold ones override standard browser shortcuts: 19 | 20 | Combination | Function 21 | -----------------|-------------------------------------------------------------- 22 | **Alt-b** | Open bookmarks 23 | **Ctrl-d** | Bookmark current page 24 | Ctrl-Shift-Enter | Log into current page 25 | Ctrl-Shift-s | Move downloads to a qube of your choice 26 | **Ctrl-Shift-u** | `New Identity` on steroids: Quit and restart the browser in a new disposable, with fresh Tor circuits. 27 | 28 | 29 | ## Implementation 30 | 31 | ~ 600 nonempty lines total, in a couple of Bash scripts, Awk, Python, and [JavaScript on the browser side](vm/qubes-split-browser-disp/usr/share/split-browser-disp/firefox/sb.js) (deployed as a [Mozilla AutoConfig](https://support.mozilla.org/en-US/kb/customizing-firefox-using-autoconfig) file). The bookmark and login managers use [rofi](https://github.com/davatorium/rofi) in dmenu mode. 32 | 33 | 34 | ## Bookmarks 35 | 36 | Bookmarks are stored in a text file, `~/.local/share/split-browser/bookmarks.tsv`. Each line consists of a timestamp, URL, and title, separated by tabs. 37 | 38 | The bookmark manager can instantly search through tens of thousands of bookmarks. 39 | 40 | To reduce attack surface, only printable ASCII characters are allowed by default. This can be broadened to UTF-8: Symlink `[/usr/local]/etc/split-browser/20-utf-8.bash.EXAMPLE` without the `.EXAMPLE` suffix. 41 | 42 | 43 | ## Logins 44 | 45 | Login credentials are stored in a freely organizable, arbitrarily nested directory tree `~/.local/share/split-browser/logins/`, where each database entry (e.g. `rusty/github/factor1/`) is a directory containing a `urls.txt` file with patterns, one per line. A pattern's first letter decides how it is interpreted: 46 | 47 | First letter | Type | Scope 48 | :-----------:|----------------|------------------------------------------------- 49 | `=` | Literal string | Must match whole URL. 50 | `~` | Regex | Must match whole URL. 51 | `^` | Literal string | Must match beginning of URL. The rest of the URL is considered to match if it starts with (or if the pattern ends with) `/`, `?`, or `#`. 52 | 53 | If any of the lines match and the user subsequently chooses this database entry, the `login` executable in the directory is called - if missing, it defaults to `split-browser-login-fields` in `$PATH`: 54 | 55 | `split-browser-login-fields` goes through each filename in the `fields/` child directory, in lexical order. If it ends in `.txt` (and isn't executable), the file's _content_ is collected. If it is executable (and doesn't end in `.txt`), its _output_ is collected instead. All these collected fields are finally "auto-typed" into the browser using fake key presses, with Tab between fields and Enter after the last. 56 | 57 | **To get started, just try the login keyboard shortcut (Ctrl-Shift-Enter) on any login page.** This will prompt you to create a skeleton directory that will become the database entry for the page, and pop up a terminal window there so you can have a look around, save your username, and possibly change the generated password or trim junk off the URL. Then ensure that the browser's focus is on the username field and press the keyboard shortcut again. 58 | 59 | Here's an example of how a login directory structure could be organized: 60 | 61 | ~/.local/share/split-browser/logins/ 62 | rusty/ 63 | github/ 64 | factor1/ 65 | urls.txt: ^https://github.com/login 66 | fields/ 67 | 01-user.txt: rustybird 68 | 02-pass.txt: correct horse battery staple 69 | factor2/ 70 | urls.txt: =https://github.com/sessions/two-factor/app 71 | fields/ 72 | 01-totp: #!/bin/sh 73 | oathtool --totp --base32 foobarba7qux 74 | ... 75 | 76 | _TODO: set up an automounted encrypted filesystem?_ 77 | 78 | _TODO: build some sort of KeePassXC bridge?_ 79 | 80 | 81 | ## Notes 82 | 83 | - Multiple Split Browser instances (e.g. one with Tor Browser's Security Level set to Standard and another set to Safest) can run in parallel, even from the same persistent qube. This won't corrupt the bookmark and login collections. 84 | 85 | - If you're starting Split Browser through its application launcher shortcuts, any diagnostic messages go into the syslog of the persistent qube: 86 | 87 | journalctl -t qubes.StartApp+split-browser-dom0 \ 88 | -t qubes.StartApp+split-browser-safest-dom0 89 | 90 | - Non-"Tor Browser" versions of Firefox should also work: Symlink `[/usr/local]/etc/split-browser-disp/22-firefox.bash.EXAMPLE` (or copy it, if you need to adjust the Firefox location) without the `.EXAMPLE` suffix. 91 | 92 | 93 | ## Installation 94 | 95 | 1. Create a new persistent qube or take an existing one, and configure it to launch the right disposables and (optionally, for safety against user error) to have no network access itself: 96 | 97 | qvm-create --label=purple surfer 98 | qvm-prefs surfer default_dispvm whonix-ws-XX-dvm 99 | qvm-prefs surfer netvm '' 100 | 101 | The disposables will know which persistent qube launched them, so don't name it "rumplestiltskin" if an exploited browser mustn't find out. 102 | 103 | 2. Install the `qubes-split-browser` package from [qubes-repo-contrib](https://www.qubes-os.org/doc/installing-contributed-packages/) in your persistent qube's TemplateVM (e.g. fedora-XX). 104 | 105 | _Or install manually:_ Copy `vm/` into your persistent qube or its TemplateVM (e.g. fedora-XX) and run `sudo make install-persist`; then install the `rofi oathtool` packages in the TemplateVM. 106 | 107 | 3. Install the `qubes-split-browser-disp` package from qubes-repo-contrib in your persistent qube's default disposable template's TemplateVM (e.g. whonix-ws-XX). 108 | 109 | _Or install manually:_ Copy `vm/` into your persistent qube's default disposable template (e.g. whonix-ws-XX-dvm) or the latter's TemplateVM (e.g. whonix-ws-XX) and run `sudo make install-disp`; then install the `xdotool` package in the TemplateVM. 110 | 111 | Either way, also ensure that an extracted Tor Browser will be available in `~/.tb/tor-browser/` (e.g. by running the Tor Browser Downloader `update-torbrowser` in whonix-ws-XX). 112 | 113 | 4. You can enable the Split Browser application launcher shortcuts for your persistent qube as usual through the Applications tab in Qube Settings, or alternatively run `split-browser` in a terminal (with `-h` to see the help message). 114 | 115 | _TODO: consider recommending `systemctl disable onion-grater` in whonix-gw-XX, because Split Browser doesn't need to access the tor control port at all_ 116 | -------------------------------------------------------------------------------- /archlinux/PKGBUILD.in: -------------------------------------------------------------------------------- 1 | # Maintainer: Rusty Bird 2 | pkgname=( qubes-split-browser{,-disp} ) 3 | pkgver=@VERSION@ 4 | pkgrel=@REL@ 5 | pkgdesc="Split Browser for Qubes OS" 6 | arch=( any ) 7 | license=( BSD ) 8 | url=https://github.com/rustybird/qubes-app-split-browser 9 | _pkgnvr="${pkgname}-${pkgver}-${pkgrel}" 10 | source=("${_pkgnvr}.tar.gz") 11 | sha256sums=(SKIP) 12 | 13 | _backup() { 14 | readarray -d '' backup < <(set -e -o pipefail 15 | cd -- "$pkgdir" 16 | find "${@?}" -type f -print0 | sort -z) 17 | wait $! 18 | } 19 | 20 | package_qubes-split-browser() { 21 | depends=( 22 | awk 23 | bash 24 | coreutils 25 | libnotify 26 | python 27 | rofi 28 | systemd 29 | ) 30 | optdepends=( 31 | 'oath-toolkit: authenticate by TOTP' 32 | ) 33 | cd "${_pkgnvr}" 34 | make -C vm PREFIX=/usr DESTDIR="$pkgdir" install-persist 35 | install -D -m 644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE-0BSD 36 | install -D -m 644 -t "$pkgdir/usr/share/doc/$pkgname/" README.md 37 | _backup etc/split-browser/ 38 | } 39 | 40 | package_qubes-split-browser-disp() { 41 | pkgdesc='Split Browser for Qubes OS (disposable side)' 42 | depends=( 43 | bash 44 | coreutils 45 | socat 46 | systemd 47 | ) 48 | optdepends=( 49 | 'xdotool: autotype logins' 50 | ) 51 | cd "${_pkgnvr}" 52 | make -C vm PREFIX=/usr DESTDIR="$pkgdir" install-disp 53 | install -D -m 644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE-0BSD 54 | _backup etc/split-browser-disp/ 55 | } 56 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | qubes-split-browser (0.16.6-1) unstable; urgency=medium 2 | 3 | * (packaging): debian: Update standards version 4 | * Disable Firefox's password manager asking to save password 5 | * sb.js: Fix compatibility with Firefox 136+ 6 | 7 | -- Rusty Bird Wed, 30 Apr 2025 22:24:38 +0000 8 | 9 | qubes-split-browser (0.16.5-1) unstable; urgency=medium 10 | 11 | * Assume that Mullvad Browser was extracted in ~/.local/share/ 12 | * Rip out old cleanup code 13 | * (packaging): debian: Update standards version 14 | * Hide speech synthesis errors in Reader Mode etc. 15 | * Update copyright year 16 | 17 | -- Rusty Bird Sat, 18 Jan 2025 11:47:08 +0000 18 | 19 | qubes-split-browser (0.16.4-1) unstable; urgency=medium 20 | 21 | * Update copyright year 22 | * Add blank homepage to 32-ui-tweaks.js.EXAMPLE 23 | * Add Mullvad Browser example configuration 24 | 25 | -- Rusty Bird Thu, 06 Jun 2024 11:14:13 +0000 26 | 27 | qubes-split-browser (0.16.3-1) unstable; urgency=medium 28 | 29 | * x11-unoverride-redirect: Avoid GetGeometry() call 30 | * x11-unoverride-redirect: Avoid GetWindowAttributes() call too 31 | * sb.js: add blanktab.html to URLs skipped for bookmark/login 32 | * Use rofi instead of dmenu (temporarily?) 33 | 34 | -- Rusty Bird Mon, 18 Dec 2023 17:50:17 +0000 35 | 36 | qubes-split-browser (0.16.2-1) unstable; urgency=medium 37 | 38 | * (packaging): archlinux: Fix URL 39 | * (packaging): debian: Add 'Rules-Requires-Root: no' 40 | * (packaging): Drop builder v1 exceptions for CentOS 7, Debian 8 41 | * "VM" -> "qube", etc. 42 | * README.md: Clarify "qube" for people unfamiliar with Qubes OS 43 | * 32-ui-tweaks.js.EXAMPLE: Hide bookmarks toolbar on about:newtab 44 | * 21-tor-browser.bash: Set new TOR_PROVIDER variable for TB 13 45 | 46 | -- Rusty Bird Mon, 09 Oct 2023 16:13:43 +0000 47 | 48 | qubes-split-browser (0.16.1-1) unstable; urgency=medium 49 | 50 | * Add builderv2 integration 51 | * archlinux: update packaging 52 | 53 | -- Frédéric Pierret (fepitre) Sun, 06 Aug 2023 10:26:32 +0200 54 | 55 | qubes-split-browser (0.16.0-1) unstable; urgency=medium 56 | 57 | * README.md: Update URL in example login directory structure 58 | * GNUmakefile: Don't fail if update-desktop-database is missing 59 | * GNUmakefile: Sync app menus even on AppVM 60 | * Remove orphaned pref 61 | * Update download warning pref for Tor Browser 12.5 62 | * Generate passwords without pwgen 63 | * Default to shorter passwords from a larger range of characters 64 | 65 | -- Rusty Bird Sun, 25 Jun 2023 14:30:05 +0000 66 | 67 | qubes-split-browser (0.15.5-1) unstable; urgency=medium 68 | 69 | * (packaging): debian: Update standards version 70 | * Update copyright year 71 | * split-browser: Increase pipe capacity 72 | 73 | -- Rusty Bird Mon, 16 Jan 2023 12:57:19 +0000 74 | 75 | qubes-split-browser (0.15.4-1) unstable; urgency=medium 76 | 77 | * split-browser: Fix '--safest' Security Level for Tor Browser 12 78 | 79 | -- Rusty Bird Wed, 07 Dec 2022 21:55:55 +0000 80 | 81 | qubes-split-browser (0.15.3-1) unstable; urgency=medium 82 | 83 | * Hide display language notification 84 | 85 | -- Rusty Bird Wed, 07 Dec 2022 20:15:54 +0000 86 | 87 | qubes-split-browser (0.15.2-1) unstable; urgency=medium 88 | 89 | * split-browser: Simplify Python oneliner 90 | * split-browser-{login,bookmark}: Allow notify-send to fail 91 | 92 | -- Rusty Bird Wed, 30 Nov 2022 14:23:52 +0000 93 | 94 | qubes-split-browser (0.15.1-1) unstable; urgency=medium 95 | 96 | * (packaging): debian: Bump debhelper compat to 12 97 | * sb.js: Ensure Ctrl-Shift-s error messages can be displayed 98 | * split-browser-bookmark: Fix XML-escaping with Bash 5.2+ 99 | 100 | -- Rusty Bird Tue, 15 Nov 2022 09:34:44 +0000 101 | 102 | qubes-split-browser (0.15.0-1) unstable; urgency=medium 103 | 104 | [ Frédéric Pierret (fepitre) ] 105 | * Add gitlab-ci 106 | 107 | [ Rusty Bird ] 108 | * README.md: Update link 109 | * Add $SB_DISP variable to override default @dispvm 110 | * (packaging): debian: Update standards version 111 | 112 | -- Rusty Bird Mon, 30 May 2022 10:31:18 +0000 113 | 114 | qubes-split-browser (0.14.1-1) unstable; urgency=medium 115 | 116 | * (packaging): debian: Update standards version 117 | * Update copyright year 118 | * Remove obsolete pref 119 | * Fix occasional browser launch failure in DisposableVM 120 | 121 | -- Rusty Bird Wed, 16 Mar 2022 11:34:41 +0000 122 | 123 | qubes-split-browser (0.14.0-1) unstable; urgency=medium 124 | 125 | * Revert "(packaging): Don't hardcode package release number" 126 | * README.md: Reformat example login directory structure 127 | * Code style tweaks 128 | * Remove obsolete pref 129 | * (packaging): rpm_spec: Sort file list alphabetically 130 | * x11-unoverride-redirect: Fix pylint consider-using-with warning 131 | * sb.js: Inline newTab() 132 | * Support multiple URL arguments for 'split-browser-cmd newtab' 133 | 134 | -- Rusty Bird Tue, 28 Sep 2021 17:36:40 +0000 135 | 136 | qubes-split-browser (0.13.1-1) unstable; urgency=medium 137 | 138 | [ Rusty Bird ] 139 | * 140 | 141 | -- Frédéric Pierret (fepitre) Mon, 10 May 2021 13:50:00 +0200 142 | 143 | qubes-split-browser (0.13.0-2) unstable; urgency=medium 144 | 145 | * Update copyright year 146 | * (packaging): Don't hardcode package release number 147 | * (packaging): rpm_spec: Add 'make' build dependency for fc34 148 | 149 | -- Rusty Bird Sun, 09 May 2021 20:00:02 +0000 150 | 151 | qubes-split-browser (0.13.0-1) unstable; urgency=medium 152 | 153 | * README.md: Rephrase 154 | * README.md: Uppercase "XX" version placeholder, as in qubes-doc 155 | * README.md: "default DisposableVM Template", as in Qube Settings 156 | * README.md: Add qubes-repo-contrib install instructions 157 | * Remove cruft 158 | * (packaging): debian: Update standards version 159 | * sb.js: Code style tweaks 160 | * Register Split Browser as an available HTTP(S) handler 161 | 162 | -- Rusty Bird Thu, 17 Dec 2020 13:32:58 +0000 163 | 164 | qubes-split-browser (0.12.4-1) unstable; urgency=medium 165 | 166 | [ Rusty Bird ] 167 | * Remove obsolete (Firefox ESR 68 era) prefs 168 | * Code style tweaks 169 | * split-browser-login: Remove stray space 170 | * Rename this Git repo to qubes-app-split-browser 171 | 172 | -- Frédéric Pierret (fepitre) Mon, 30 Nov 2020 11:52:50 +0100 173 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: qubes-split-browser 2 | Section: web 3 | Priority: optional 4 | Maintainer: Rusty Bird 5 | Build-Depends: debhelper-compat (= 12) 6 | Standards-Version: 4.7.2 7 | Rules-Requires-Root: no 8 | Homepage: https://github.com/rustybird/qubes-app-split-browser 9 | Vcs-Git: https://github.com/rustybird/qubes-app-split-browser.git 10 | 11 | Package: qubes-split-browser 12 | Architecture: all 13 | Depends: 14 | libnotify-bin, 15 | python3, 16 | rofi, 17 | systemd, 18 | ${misc:Depends} 19 | Suggests: oathtool 20 | Description: Split Browser for Qubes OS 21 | Handle persistent bookmarks and logins in a Qubes OS qube, and communicate with 22 | throwaway Tor Browser (or Firefox) instances in disposables. 23 | 24 | Package: qubes-split-browser-disp 25 | Architecture: all 26 | Depends: 27 | socat, 28 | systemd, 29 | ${misc:Depends} 30 | Recommends: xdotool 31 | Description: Split Browser for Qubes OS (disposable side) 32 | Present a throwaway Tor Browser (or Firefox) instance in a Qubes OS disposable, 33 | and communicate with a qube that handles persistent bookmarks and logins. 34 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | ../LICENSE-0BSD -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /debian/patches/esr.diff: -------------------------------------------------------------------------------- 1 | --- a/vm/qubes-split-browser-disp/etc/split-browser-disp/22-firefox.bash.EXAMPLE 2 | +++ b/vm/qubes-split-browser-disp/etc/split-browser-disp/22-firefox.bash.EXAMPLE 3 | @@ -1,2 +1,2 @@ 4 | -SB_FIREFOX_DIR=/usr/lib/firefox 5 | +SB_FIREFOX_DIR=/usr/lib/firefox-esr 6 | SB_FIREFOX=( ./firefox-bin ) 7 | -------------------------------------------------------------------------------- /debian/patches/series: -------------------------------------------------------------------------------- 1 | esr.diff 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | 6 | override_dh_auto_install: 7 | $(MAKE) -C vm PREFIX=/usr \ 8 | DESTDIR='$(CURDIR)/debian/qubes-split-browser' \ 9 | install-persist 10 | $(MAKE) -C vm PREFIX=/usr \ 11 | DESTDIR='$(CURDIR)/debian/qubes-split-browser-disp' \ 12 | install-disp 13 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /rpm_spec/lib64.diff: -------------------------------------------------------------------------------- 1 | --- a/vm/qubes-split-browser-disp/etc/split-browser-disp/22-firefox.bash.EXAMPLE 2 | +++ b/vm/qubes-split-browser-disp/etc/split-browser-disp/22-firefox.bash.EXAMPLE 3 | @@ -1,2 +1,2 @@ 4 | -SB_FIREFOX_DIR=/usr/lib/firefox 5 | +SB_FIREFOX_DIR=/usr/lib64/firefox 6 | SB_FIREFOX=( ./firefox-bin ) 7 | -------------------------------------------------------------------------------- /rpm_spec/qubes-split-browser.spec.in: -------------------------------------------------------------------------------- 1 | Name: qubes-split-browser 2 | Version: @VERSION@ 3 | Release: 1%{?dist} 4 | BuildArch: noarch 5 | License: 0BSD 6 | URL: https://github.com/rustybird/qubes-app-split-browser 7 | Source0: %{name}-%{version}.tar.gz 8 | 9 | BuildRequires: make 10 | Requires: %{_bindir}/awk 11 | Requires: bash >= 4.4 12 | Requires: coreutils 13 | Requires: libnotify 14 | Requires: python3 15 | Requires: rofi 16 | Requires: systemd 17 | Recommends: oathtool 18 | Summary: Split Browser for Qubes OS 19 | 20 | %description 21 | Handle persistent bookmarks and logins in a Qubes OS qube, and communicate with 22 | throwaway Tor Browser (or Firefox) instances in disposables. 23 | 24 | %prep 25 | %setup -q 26 | patch -p1 -i rpm_spec/lib64.diff 27 | 28 | %install 29 | gmake -C vm PREFIX=/usr DESTDIR="$RPM_BUILD_ROOT" install-persist install-disp 30 | 31 | %files 32 | %license LICENSE-0BSD 33 | %doc README.md 34 | %config(noreplace) /etc/split-browser/ 35 | /usr/bin/* 36 | /usr/lib/tmpfiles.d/split-browser.conf 37 | /usr/share/applications/* 38 | 39 | %package disp 40 | Requires: bash >= 4.4 41 | Requires: coreutils 42 | Requires: socat 43 | Requires: systemd 44 | Recommends: xdotool 45 | Summary: Split Browser for Qubes OS (disposable side) 46 | 47 | %description disp 48 | Present a throwaway Tor Browser (or Firefox) instance in a Qubes OS disposable, 49 | and communicate with a qube that handles persistent bookmarks and logins. 50 | 51 | %files disp 52 | %license LICENSE-0BSD 53 | %config(noreplace) /etc/split-browser-disp/ 54 | /etc/qubes-rpc/* 55 | /usr/lib/tmpfiles.d/split-browser-disp.conf 56 | /usr/share/split-browser-disp/ 57 | 58 | %changelog 59 | @CHANGELOG@ 60 | -------------------------------------------------------------------------------- /scripts/run-eslint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit 5 | 6 | readarray -d '' files < <(git ls-files -z '*.js') 7 | wait $! 8 | 9 | set -x 10 | eslint "$@" -- "${files[@]?}" 11 | -------------------------------------------------------------------------------- /scripts/run-pylint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit 5 | 6 | readarray -d '' files < <(git grep -zl '^#!/usr/bin/python') 7 | wait $! 8 | 9 | set -x 10 | pylint "$@" -- "${files[@]?}" 11 | -------------------------------------------------------------------------------- /scripts/run-shellcheck: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit 5 | 6 | readarray -d '' programs < <(git grep -zl '^#!/bin/bash') 7 | wait $! 8 | readarray -d '' configs < <(git ls-files -z '*.bash*') 9 | wait $! 10 | 11 | set -x 12 | shellcheck "$@" -- "${programs[@]?}" 13 | shellcheck --shell=bash --exclude=SC2034 "$@" -- "${configs[@]?}" 14 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.16.6 2 | -------------------------------------------------------------------------------- /vm/GNUmakefile: -------------------------------------------------------------------------------- 1 | ifeq ($(PREFIX),) 2 | ifeq ($(shell qubesdb-read /qubes-vm-persistence),full) 3 | PREFIX = /usr 4 | else 5 | PREFIX = /usr/local 6 | endif 7 | else 8 | # strip a trailing slash, e.g. from tab completion 9 | override PREFIX := $(PREFIX:/=) 10 | endif 11 | 12 | ifeq ($(PREFIX),/usr) 13 | etc = /etc 14 | else 15 | ifeq ($(PREFIX),/usr/local) 16 | etc = $(PREFIX)/etc 17 | else 18 | $(error PREFIX must be /usr or /usr/local) 19 | endif 20 | endif 21 | 22 | .ONESHELL: 23 | .SHELLFLAGS += -e -x 24 | .SILENT: 25 | 26 | default: 27 | 28 | install-persist: 29 | umask 022 30 | mkdir -p -- '$(DESTDIR)$(PREFIX)/' '$(DESTDIR)$(etc)/' 31 | cp -R -- qubes-split-browser/usr/* '$(DESTDIR)$(PREFIX)/' 32 | cp -R -- qubes-split-browser/etc/* '$(DESTDIR)$(etc)/' 33 | ifeq ($(DESTDIR),) 34 | systemd-tmpfiles --create split-browser.conf 35 | if type update-desktop-database >/dev/null; then update-desktop-database; fi 36 | qrexec-client-vm dom0 qubes.SyncAppMenus /etc/qubes-rpc/qubes.GetAppmenus 37 | endif 38 | 39 | install-disp: 40 | umask 022 41 | mkdir -p -- '$(DESTDIR)$(PREFIX)/' '$(DESTDIR)$(etc)/' 42 | cp -R -- qubes-split-browser-disp/usr/* '$(DESTDIR)$(PREFIX)/' 43 | cp -R -- qubes-split-browser-disp/etc/* '$(DESTDIR)$(etc)/' 44 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/etc/qubes-rpc/split-browser-disp: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit nullglob 5 | export -n IFS=$'\t' 6 | exec {saved_stdout_fd}>&1 >&2 7 | 8 | # shellcheck disable=SC1090 9 | for f in {,/usr/local}/etc/split-browser-disp/*.bash; do source "$f"; done 10 | export SB_INTO_FIREFOX=/run/split-browser-disp/into-firefox \ 11 | SB_FROM_FIREFOX=/run/split-browser-disp/from-firefox 12 | 13 | 14 | while IFS= read -r cmd_line; do 15 | readarray -d $'\t' -t cmd <<<"$cmd_line" 16 | cmd[-1]=${cmd[-1]%$'\n'} 17 | 18 | case "${cmd[0]}" in 19 | setup) 20 | cd -- "$SB_FIREFOX_DIR" 21 | 22 | [[ -w . && -w defaults/pref/ ]] || 23 | sudo install -d -m u+rwx -o "$EUID" . defaults/pref/ 24 | f=( /[u]sr{/local,}/share/split-browser-disp/firefox/sb-load.js ) 25 | ln -s "${f[0]}" defaults/pref/ 26 | f=( /[u]sr{/local,}/share/split-browser-disp/firefox/sb.js ) 27 | printf '%s\n' '' "${cmd[@]:1}" ';' | cat - "${f[0]}" >sb.js 28 | 29 | mkfifo -- "$SB_FROM_FIREFOX" 30 | cat <"$SB_FROM_FIREFOX" >&"$saved_stdout_fd" & 31 | # shellcheck disable=SC2064 32 | trap "wait $! || exit \$?" EXIT 33 | trap 'exit 0' USR1 34 | trap 'exit 1' USR2 35 | ;; 36 | master) 37 | ( 38 | "${SB_FIREFOX[@]}" "${cmd[@]:1}" && sig=USR1 || sig=USR2 39 | kill -s "$sig" $$ 40 | ) & 41 | ;; 42 | helper) 43 | "${cmd[@]:1}" 44 | ;; 45 | autotype) 46 | if type xdotool >/dev/null; then xdotool type -- "${cmd[*]:1}"; fi 47 | ;; 48 | newtab) 49 | socat -u STDIN UNIX-CONNECT:"$SB_INTO_FIREFOX" <<<"${cmd[*]:1}" 50 | ;; 51 | *) 52 | printf 'Unknown command from persistent side: %s\n' "${cmd[0]}" 53 | ;; 54 | esac 55 | done 56 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/etc/split-browser-disp/21-tor-browser.bash: -------------------------------------------------------------------------------- 1 | SB_FIREFOX_DIR=~/.tb/tor-browser/Browser 2 | SB_FIREFOX=( ./start-tor-browser ) 3 | 4 | unset TOR_DEFAULT_HOMEPAGE 5 | unset TOR_SOCKS_IPC_PATH 6 | export TOR_SOCKS_HOST=10.152.152.10 7 | export TOR_SKIP_LAUNCH=1 8 | export TOR_PROVIDER=none 9 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/etc/split-browser-disp/22-firefox.bash.EXAMPLE: -------------------------------------------------------------------------------- 1 | SB_FIREFOX_DIR=/usr/lib/firefox 2 | SB_FIREFOX=( ./firefox-bin ) 3 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/etc/split-browser-disp/23-mullvad-browser.bash.EXAMPLE: -------------------------------------------------------------------------------- 1 | SB_FIREFOX_DIR=~/.local/share/mullvad-browser/Browser 2 | SB_FIREFOX=( ./start-mullvad-browser ) 3 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/usr/lib/tmpfiles.d/split-browser-disp.conf: -------------------------------------------------------------------------------- 1 | d /run/split-browser-disp - user user 2 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/usr/share/split-browser-disp/firefox/sb-load.js: -------------------------------------------------------------------------------- 1 | pref("general.config.filename", "sb.js"); 2 | pref("general.config.obscure_value", 0); 3 | pref("general.config.sandbox_enabled", false); 4 | -------------------------------------------------------------------------------- /vm/qubes-split-browser-disp/usr/share/split-browser-disp/firefox/sb.js: -------------------------------------------------------------------------------- 1 | 2 | (() => { 3 | "use strict"; 4 | 5 | const CC = Components.Constructor; 6 | 7 | const { ReaderMode } = ChromeUtils.importESModule( 8 | "resource://gre/modules/ReaderMode.sys.mjs"); 9 | const { Subprocess } = ChromeUtils.importESModule( 10 | "resource://gre/modules/Subprocess.sys.mjs"); 11 | 12 | const AppStartup = Cc["@mozilla.org/toolkit/app-startup;1"] 13 | .getService(Ci.nsIAppStartup); 14 | const Environment = Cc["@mozilla.org/process/environment;1"] 15 | .getService(Ci.nsIEnvironment); 16 | const IoService = Cc["@mozilla.org/network/io-service;1"] 17 | .getService(Ci.nsIIOService); 18 | const ObserverService = Cc["@mozilla.org/observer-service;1"] 19 | .getService(Ci.nsIObserverService); 20 | const PrefBranch = Cc["@mozilla.org/preferences-service;1"] 21 | .getService(Ci.nsIPrefBranch); 22 | const ScriptSecurity = Cc["@mozilla.org/scriptsecuritymanager;1"] 23 | .getService(Ci.nsIScriptSecurityManager); 24 | const WindowMediator = Cc["@mozilla.org/appshell/window-mediator;1"] 25 | .getService(Ci.nsIWindowMediator); 26 | const WindowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"] 27 | .getService(Ci.nsIWindowWatcher); 28 | 29 | const ConvInputStream = CC("@mozilla.org/intl/converter-input-stream;1", 30 | Ci.nsIConverterInputStream, "init"); 31 | const ConvOutputStream = CC("@mozilla.org/intl/converter-output-stream;1", 32 | Ci.nsIConverterOutputStream, "init"); 33 | const File = CC("@mozilla.org/file/local;1", 34 | Ci.nsIFile, "initWithPath"); 35 | const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1", 36 | Ci.nsIFileOutputStream, "init"); 37 | const PrBool = CC("@mozilla.org/supports-PRBool;1", 38 | Ci.nsISupportsPRBool); 39 | const UnixServerSocket = CC("@mozilla.org/network/server-socket;1", 40 | Ci.nsIServerSocket, "initWithFilename"); 41 | 42 | const FieldSep = "\t"; 43 | const RecordSep = "\n"; 44 | const BadByte = new RegExp([FieldSep, RecordSep, "\0"].join("|"), "g"); 45 | const IntoFirefox = new UnixServerSocket( 46 | new File(Environment.get("SB_INTO_FIREFOX")), 47 | 0o644, -1); 48 | const FromFirefox = new ConvOutputStream( 49 | new FileOutputStream( 50 | new File(Environment.get("SB_FROM_FIREFOX")), 51 | 0x02, -1, 0), 52 | "UTF-8"); 53 | 54 | const sendReq = (...fields) => { 55 | try { 56 | FromFirefox.writeString(fields.join(FieldSep) + RecordSep); 57 | } catch(e) { 58 | FromFirefox.close(); 59 | throw e; 60 | } 61 | }; 62 | 63 | const sendReqWithPageInfo = (...fields) => { 64 | const browser = WindowMediator.getMostRecentBrowserWindow().gBrowser; 65 | const titleForUtf8 = browser.contentTitle.replace(BadByte, " "); 66 | const titleForAscii = titleForUtf8.normalize("NFKD"); 67 | let uri = browser.currentURI; 68 | 69 | if ([ 70 | "about:blank", 71 | "about:newtab", 72 | "chrome://browser/content/blanktab.html", 73 | ].includes(uri.asciiSpec)) 74 | return; 75 | 76 | const originalUrl = ReaderMode.getOriginalUrl(uri.asciiSpec); 77 | if (originalUrl) 78 | uri = IoService.newURI(originalUrl); 79 | 80 | let urlForUtf8; 81 | try { 82 | urlForUtf8 = decodeURI(uri.displaySpec); 83 | if (urlForUtf8.indexOf("%") !== -1 || urlForUtf8.search(BadByte) !== -1) 84 | throw new URIError; 85 | } catch ({}) { 86 | urlForUtf8 = uri.displaySpec; 87 | } 88 | 89 | sendReq(...fields, uri.asciiSpec, titleForAscii, urlForUtf8, titleForUtf8); 90 | }; 91 | 92 | const restart = () => { 93 | const cancel = new PrBool; 94 | 95 | ObserverService.notifyObservers(cancel, "quit-application-requested", null); 96 | 97 | if (!cancel.data) { 98 | sendReq("restart"); 99 | AppStartup.quit(Ci.nsIAppStartup.eAttemptQuit); 100 | } 101 | }; 102 | 103 | const moveDownloads = () => 104 | Subprocess.call({ 105 | command: "/bin/bash", 106 | arguments: ["-lc", 107 | "exec /usr/lib/qubes/qvm-move-to-vm.kde * &>/dev/null"], 108 | environment: { LC_CTYPE: "C.UTF-8" }, 109 | workdir: PrefBranch.getComplexValue("browser.download.dir", 110 | Ci.nsIPrefLocalizedString).data 111 | }); 112 | 113 | const onKey = e => { 114 | const k = e.key.toLowerCase(); 115 | let f; 116 | 117 | if (!e.altKey && e.shiftKey && e.ctrlKey && !e.metaKey && k === "enter") 118 | f = () => sendReqWithPageInfo("login", "get"); 119 | else if (!e.altKey && !e.shiftKey && e.ctrlKey && !e.metaKey && k === "d") 120 | f = () => sendReqWithPageInfo("bookmark", "add"); 121 | else if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && k === "b") 122 | f = () => sendReq("bookmark", "get"); 123 | else if (!e.altKey && e.shiftKey && e.ctrlKey && !e.metaKey && k === "s") 124 | f = moveDownloads; 125 | else if (!e.altKey && e.shiftKey && e.ctrlKey && !e.metaKey && k === "u") 126 | f = restart; 127 | else 128 | return; 129 | 130 | e.preventDefault(); 131 | if (e.type === "keydown") 132 | f(); 133 | }; 134 | 135 | const perWindowHotkeys = addOrRemoveEventListener => { 136 | addOrRemoveEventListener("keydown", onKey, true); 137 | addOrRemoveEventListener("keyup", onKey, true); 138 | }; 139 | 140 | const isMainWindow = win => 141 | win.document.documentElement.getAttribute("windowtype") === 142 | "navigator:browser"; 143 | 144 | const windowReady = e => { 145 | const win = e.currentTarget; 146 | 147 | win.removeEventListener(e.type, windowReady, true); 148 | if (isMainWindow(win)) 149 | perWindowHotkeys(win.addEventListener); 150 | }; 151 | 152 | 153 | // attach hotkey listener to any new main window 154 | WindowWatcher.registerNotification({ 155 | observe: (win, topic) => { 156 | if (topic === "domwindowopened") { 157 | /* In this block, the DOM in DOMContentLoaded is that of the 158 | * Firefox GUI, not of any website. Before the GUI has loaded, 159 | * isMainWindow() can return false negatives. 160 | */ 161 | win.addEventListener("DOMContentLoaded", windowReady, true); 162 | if (isMainWindow(win)) { 163 | win.removeEventListener("DOMContentLoaded", windowReady, true); 164 | perWindowHotkeys(win.addEventListener); 165 | } 166 | } else if (topic === "domwindowclosed" && isMainWindow(win)) 167 | perWindowHotkeys(win.removeEventListener); 168 | } 169 | }); 170 | 171 | // listen for URL load commands from the persistent qube 172 | IntoFirefox.asyncListen({ 173 | onSocketAccepted: ({}, transport) => { 174 | const input = new ConvInputStream( 175 | transport.openInputStream( 176 | Ci.nsITransport.OPEN_BLOCKING, 0, 0), 177 | "UTF-8", 0, 0); 178 | const buf = {}; 179 | let line = ""; 180 | 181 | try { 182 | while (input.readString(-1, buf) !== 0) 183 | line += buf.value; 184 | } finally { 185 | input.close(); 186 | } 187 | 188 | if (line.slice(-1) !== RecordSep) 189 | return; 190 | 191 | const urls = line.slice(0, -1).split(FieldSep); 192 | const browser = WindowMediator.getMostRecentBrowserWindow().gBrowser; 193 | const params = { 194 | skipAnimation: urls.length > 1, 195 | triggeringPrincipal: ScriptSecurity.getSystemPrincipal(), 196 | fromExternal: true 197 | }; 198 | 199 | browser.selectedTab = urls.map(url => browser.addTab(url, params))[0]; 200 | } 201 | }); 202 | })(); 203 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/10-defaults.bash: -------------------------------------------------------------------------------- 1 | SB_DATA_DIR=${SB_DATA_DIR:-${XDG_DATA_HOME:-~/.local/share}/split-browser} 2 | SB_LOGIN_DIR=${SB_LOGIN_DIR:-$SB_DATA_DIR/logins} 3 | SB_BOOKMARK_FILE=${SB_BOOKMARK_FILE:-$SB_DATA_DIR/bookmarks.tsv} 4 | SB_DISP=${SB_DISP:-@dispvm} 5 | 6 | SB_LOGIN_SECURITY_DELAY_SECONDS=1 7 | SB_LOGIN_PASSGEN_LEN=20 8 | SB_LOGIN_PASSGEN_ALPHABET='\041-\176' # printable ASCII (no space), tr syntax 9 | #SB_LOGIN_PASSGEN_ALPHABET='a-zA-Z0-9' 10 | SB_BOOKMARK_PRETTY_DATE_LEN=19 # with time zone: 25 11 | SB_BOOKMARK_PRETTY_TITLE_LEN=70 12 | SB_ELLIPSIS=$'\u2026' 13 | SB_STAR=$'\u2605' 14 | 15 | sb_dmenu() { 16 | rofi -dmenu \ 17 | -normal-window \ 18 | -window-title "Split Browser - $SB_SCREEN" \ 19 | -theme-str 'window {width: 100%;}' \ 20 | -theme solarized \ 21 | "$@" 22 | } 23 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/20-utf-8.bash.EXAMPLE: -------------------------------------------------------------------------------- 1 | # allow page titles and decoded URLs beyond printable ASCII 2 | SB_CHARSET=utf-8 3 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/prefs/11-no-updates.js: -------------------------------------------------------------------------------- 1 | pref("app.update.auto", false); 2 | pref("extensions.blocklist.enabled", false); 3 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/prefs/12-no-tor-checks.js: -------------------------------------------------------------------------------- 1 | pref("extensions.torbutton.test_enabled", false); 2 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/prefs/14-no-nags.js: -------------------------------------------------------------------------------- 1 | pref("browser.aboutConfig.showWarning", false); 2 | pref("browser.download.showTorWarning", false); 3 | pref("browser.tabs.warnOnClose", false); 4 | pref("devtools.chrome.enabled", true); 5 | pref("intl.language_notification.shown", true); 6 | pref("media.webspeech.synth.dont_notify_on_error", true); 7 | pref("privacy.prioritizeonions.showNotification", false); 8 | pref("signon.rememberSignons", false); 9 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/prefs/15-downloads-directory.js: -------------------------------------------------------------------------------- 1 | pref("browser.download.dir", "/home/user/Downloads"); 2 | pref("browser.download.folderList", 2); 3 | pref("browser.download.useDownloadDir", true); 4 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/etc/split-browser/prefs/32-ui-tweaks.js.EXAMPLE: -------------------------------------------------------------------------------- 1 | pref("browser.startup.homepage", "chrome://browser/content/blanktab.html"); 2 | pref("browser.toolbars.bookmarks.visibility", "never"); 3 | pref("browser.urlbar.trimURLs", false); 4 | pref("reader.color_scheme", "sepia"); 5 | pref("security.insecure_connection_text.enabled", true); 6 | pref("security.insecure_connection_text.pbmode.enabled", true); 7 | pref("view_source.wrap_long_lines", true); 8 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit nullglob 5 | export -n IFS=$'\t' 6 | 7 | # shellcheck disable=SC1090 8 | for f in {,/usr/local}/etc/split-browser/*.bash; do source "$f"; done 9 | export SB_CMD_INPUT="${SB_CMD_INPUT:-/run/split-browser/cmd-$$}" 10 | 11 | 12 | # Parse command-line arguments 13 | 14 | args=() 15 | cli_prefs=() 16 | 17 | for arg; do 18 | case "$arg" in 19 | -h|--help) 20 | exec cat <...] [...] 23 | 24 | 25 | --safest set Tor Browser's Security Level to Safest; equivalent to 26 | --pref='lockPref("browser.security_level.security_slider", 1);' 27 | 28 | --pref= additional browser preference line, appended after those 29 | stored in /etc/split-browser/prefs/*.js and 30 | /usr/local/etc/split-browser/prefs/*.js 31 | 32 | e.g. a URL; passed to the browser when starting (but not 33 | when restarting due to Ctrl-Shift-u) 34 | 35 | END 36 | ;; 37 | --safest|--high) # --high is deprecated 38 | cli_prefs+=( 'lockPref("browser.security_level.security_slider", 1);' ) 39 | ;; 40 | --pref=*) 41 | cli_prefs+=( "${arg#*=}" ) 42 | ;; 43 | *) 44 | args+=( "$arg" ) 45 | ;; 46 | esac 47 | done 48 | 49 | 50 | # When the disposable sends us a request, the page URL and title fields are 51 | # supplied in two versions each: printable ASCII, and UTF-8. Printable ASCII 52 | # *URLs* use Punycode IDNs and percent-encoding. Printable ASCII *titles* 53 | # actually come in as UTF-8 (normalized to NFKD, which tends to look better 54 | # than the other normalization forms after all bytes outside of the printable 55 | # ASCII range have been sanitized). 56 | 57 | [[ ${SB_CHARSET-} == utf-8 ]] || SB_CHARSET=ascii 58 | 59 | declare -A first_page_field=( [ascii]=2 [utf-8]=4 ) 60 | 61 | sanitize_ascii() { # replace any byte with _ but printable ASCII, tab, newline 62 | LC_ALL=C stdbuf -oL tr -c '\040-\176\t\n' _ 63 | } 64 | 65 | sanitize_utf-8() { # replace null byte with _; then validate as UTF-8 66 | LC_ALL=C stdbuf -oL tr '\0' _ | 67 | LC_ALL=C PYTHONIOENCODING=utf-8:strict \ 68 | python3 -Suc 'import sys; sys.stdout.writelines(sys.stdin)' 69 | } 70 | 71 | 72 | # Launch via qubes.VMShell in the disposable, because a straightforward 73 | # 'qrexec-client-vm @dispvm split-browser-disp' call would require the user to 74 | # manually create a policy in dom0. Transition to split-browser-disp with the 75 | # trick described in /usr/lib/qubes/qrun-in-vm (not reused here, because it 76 | # doesn't preserve exit status). 77 | # 78 | # Sanitize stdout and handle those requests (one per line), e.g. to get a login 79 | # credential. The handler might then send commands into the input FIFO with 80 | # split-browser-cmd. (This is essentially a crappy bidirectional RPC system 81 | # implemented *inside* a qrexec RPC call's data streams. It would be much nicer 82 | # to do multiple qrexec calls back and forth, but that requires lots of policy 83 | # configuration for each persistent qube.) Also strictly sanitize stderr and 84 | # distinguish it with a prefix. 85 | 86 | disp() ( 87 | exec {saved_stdout_fd}>&1 >&2 88 | trap 'rm -f -- "$SB_CMD_INPUT"{.tmp,}' EXIT 89 | mkfifo -- "$SB_CMD_INPUT".tmp 90 | exec {cmd_fd}<>"$SB_CMD_INPUT".tmp 91 | 92 | # 1031 is fcntl.F_SETPIPE_SZ in Python 3.10+ 93 | python3 -Sc 'import fcntl; fcntl.fcntl(0, 1031, 1024**2)' <&"$cmd_fd" || 94 | true 95 | 96 | d=/etc/qubes-rpc 97 | bash_line="PATH=/usr/local$d:$d:\$PATH exec split-browser-disp" 98 | printf '%s\n' "$bash_line" "$@" >&"$cmd_fd" 99 | mv -T -- "$SB_CMD_INPUT"{.tmp,} 100 | 101 | { 102 | qrexec-client-vm -T "$SB_DISP" qubes.VMShell+WaitForSession \ 103 | 2>&1 >&"$req_fd" {req_fd}>&- <&"$cmd_fd" | 104 | sanitize_ascii {req_fd}>&- | 105 | sed -u 's/^/disp: /' >&2 {req_fd}>&- 106 | } {req_fd}>&1 | 107 | sanitize_"$SB_CHARSET" | 108 | while IFS= read -r req_line; do 109 | readarray -d $'\t' -t req <<<"$req_line" 110 | req[-1]=${req[-1]%$'\n'} 111 | 112 | [[ $SB_CHARSET == ascii ]] || req[0]=$(sanitize_ascii <<<"${req[0]}") 113 | req=( "${req[@]::2}" "${req[@]:${first_page_field[$SB_CHARSET]}:2}" ) 114 | 115 | case "${req[0]}" in 116 | bookmark|login) 117 | # shellcheck disable=SC2145 118 | split-browser-"${req[@]}" & 119 | ;; 120 | restart) 121 | echo x >&"$saved_stdout_fd" 122 | ;; 123 | *) 124 | printf 'Unknown request from disposable side: %s\n' "${req[0]}" 125 | ;; 126 | esac 127 | done 128 | ) 129 | 130 | 131 | # Main loop: Configure and open the disposable browser. After clean shutdown, 132 | # do it again (if restart was requested). 133 | 134 | while :; do 135 | config_prefs=() 136 | for f in {,/usr/local}/etc/split-browser/prefs/*.js; do 137 | readarray -t -O ${#config_prefs[@]} config_prefs <"$f" 138 | done 139 | setup_cmd=( setup "${config_prefs[@]}" "${cli_prefs[@]}" ) 140 | master_cmd=( master "${args[@]}" ) 141 | args=() # on restart, don't load given URLs again 142 | 143 | restart=$(disp "${setup_cmd[*]}" "${master_cmd[*]}") 144 | [[ $restart ]] || exit 0 145 | done 146 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser-bookmark: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit nullglob 5 | 6 | # shellcheck disable=SC1090 7 | for f in {,/usr/local}/etc/split-browser/*.bash; do source "$f"; done 8 | 9 | 10 | notify() { 11 | local summary=$1 body=$2$'\n\n'$3 12 | 13 | # XML-escape for the notification server 14 | body=${body//'&'/'&'} 15 | body=${body//'<'/'<'} 16 | body=${body//'>'/'>'} 17 | body=${body//'"'/'"'} 18 | body=${body//"'"/'''} 19 | 20 | # Escape backslash for GLib's g_strcompress() in libnotify's notify-send 21 | # shellcheck disable=SC1003 22 | body=${body//'\'/'\\'} 23 | 24 | notify-send --expire-time=4000 -- "$summary" "$body" || true 25 | } 26 | 27 | 28 | if [[ ! -e $SB_BOOKMARK_FILE ]]; then 29 | d=$(dirname -- "$SB_BOOKMARK_FILE") 30 | mkdir -p -- "$d" 31 | : >>"$SB_BOOKMARK_FILE" 32 | sync --file-system -- "$SB_BOOKMARK_FILE" 33 | fi 34 | 35 | case "$1" in 36 | get) 37 | [[ $# == 1 ]] 38 | 39 | [[ $SB_BOOKMARK_PRETTY_DATE_LEN -le 25 ]] || 40 | SB_BOOKMARK_PRETTY_DATE_LEN=25 41 | 42 | export SB_ELLIPSIS \ 43 | SB_BOOKMARK_PRETTY_DATE_LEN \ 44 | SB_BOOKMARK_PRETTY_TITLE_LEN \ 45 | SB_BOOKMARK_PRETTY_OFS=' ' 46 | 47 | { 48 | echo # select an empty line initially 49 | split-browser-bookmark-pretty <"$SB_BOOKMARK_FILE" 50 | } | 51 | SB_SCREEN=bookmarks sb_dmenu -i -l 20 | 52 | split-browser-bookmark-pretty2url | 53 | while IFS= read -r url; do ${url:+split-browser-cmd newtab "$url"}; done 54 | ;; 55 | add) 56 | [[ $# == 3 ]] 57 | url=$2 58 | title=$3 59 | 60 | date=$(date --rfc-3339=seconds) 61 | 62 | while :; do 63 | exec {lock_fd}>>"$SB_BOOKMARK_FILE" 64 | flock -- "$lock_fd" 65 | [[ ! /dev/fd/$lock_fd -ef $SB_BOOKMARK_FILE ]] || break 66 | exec {lock_fd}>&- 67 | done 68 | 69 | if entry=$(grep -m 1 -F -- $'\t'"$url"$'\t' "$SB_BOOKMARK_FILE"); then 70 | notify 'Already bookmarked' "$url" "${entry##*$'\t'}" 71 | else 72 | entry=$date$'\t'$url$'\t'$title 73 | 74 | cp --reflink=auto -- "$SB_BOOKMARK_FILE"{,.tmp} 75 | printf '%s\n' "$entry" >>"$SB_BOOKMARK_FILE".tmp 76 | sync -- "$SB_BOOKMARK_FILE".tmp 77 | mv -T -- "$SB_BOOKMARK_FILE"{.tmp,} 78 | d=$(dirname -- "$SB_BOOKMARK_FILE") 79 | sync -- "$d" 80 | 81 | notify "$SB_STAR Page bookmarked" "$url" "$title" 82 | fi 83 | ;; 84 | esac 85 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser-bookmark-pretty: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | BEGIN { 4 | FS = "\t" 5 | OFS = ENVIRON["SB_BOOKMARK_PRETTY_OFS"] 6 | 7 | date_len = ENVIRON["SB_BOOKMARK_PRETTY_DATE_LEN"] + 0 8 | title_len = ENVIRON["SB_BOOKMARK_PRETTY_TITLE_LEN"] + 0 9 | ellipsis = ENVIRON["SB_ELLIPSIS"] 10 | 11 | if (title_len < length(ellipsis)) 12 | ellipsis = "" 13 | 14 | truncated_title_len = title_len - length(ellipsis) 15 | padding_for_titles = sprintf("%-" title_len "s", "") 16 | } 17 | 18 | { 19 | title_padding_needed = title_len - length($3) 20 | 21 | print substr($1, 1, date_len), 22 | (title_padding_needed >= 0 ? 23 | $3 substr(padding_for_titles, 1, title_padding_needed) : 24 | substr($3, 1, truncated_title_len) ellipsis), 25 | $2 26 | } 27 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser-bookmark-pretty2url: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | BEGIN { 4 | url_offset = 1 \ 5 | + ENVIRON["SB_BOOKMARK_PRETTY_DATE_LEN"] \ 6 | + ENVIRON["SB_BOOKMARK_PRETTY_TITLE_LEN"] \ 7 | + 2 * length(ENVIRON["SB_BOOKMARK_PRETTY_OFS"]) 8 | } 9 | 10 | { 11 | print substr($0, url_offset) 12 | fflush() 13 | } 14 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser-cmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -S 2 | 3 | import fcntl 4 | import os 5 | import sys 6 | 7 | with open(os.open(os.environ['SB_CMD_INPUT'], os.O_WRONLY), 'wb') as f: 8 | fcntl.flock(f, fcntl.LOCK_EX) 9 | f.write(b'\t'.join(map(os.fsencode, sys.argv[1:])) + b'\n') 10 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser-login: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit nullglob globstar 5 | 6 | # shellcheck disable=SC1090 7 | for f in {,/usr/local}/etc/split-browser/*.bash; do source "$f"; done 8 | 9 | 10 | passgen() ( 11 | export LC_ALL=C 12 | read -rN "$SB_LOGIN_PASSGEN_LEN" pass \ 13 | < <(tr -dc "$SB_LOGIN_PASSGEN_ALPHABET" "$fields_dir".tmp/"$user_textfile" 106 | passgen >"$fields_dir".tmp/"$pass_textfile" 107 | sync -- "$fields_dir".tmp/{"$user_textfile","$pass_textfile",} 108 | mv -T -- "$fields_dir"{.tmp,} 109 | sync . 110 | fi 111 | 112 | [[ ! -e "$urls_textfile" ]] || cp --reflink=auto -- "$urls_textfile"{,.tmp} 113 | printf '=%s\n' "$url" >>"$urls_textfile".tmp 114 | sync -- "$urls_textfile".tmp 115 | mv -T -- "$urls_textfile"{.tmp,} 116 | sync . 117 | 118 | exec qubes-run-terminal {lock_fd}<&- 119 | fi 120 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/split-browser-login-fields: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail 4 | shopt -s inherit_errexit nullglob 5 | 6 | 7 | fail() { 8 | printf '%s: error: %q %s\n' "${0##*/}" "$PWD/$1" "$2" >&2 9 | exit 1 10 | } 11 | 12 | 13 | fields=() 14 | 15 | for file in fields/*; do 16 | if [[ $file == *.txt ]]; then 17 | [[ ! -x $file ]] || fail "$file" 'ends in .txt but is also executable' 18 | field=$(cat "$file") 19 | else 20 | field=$("$file") 21 | fi 22 | 23 | [[ $field != *$'\t'* ]] || fail "$file" 'contains a tab character' 24 | fields+=( "${field//$'\n'/$'\r'}" ) 25 | done 26 | 27 | [[ ${#fields[@]} -gt 0 ]] || fail fields/ 'is empty' 28 | 29 | # shellcheck disable=SC2145 30 | split-browser-cmd autotype "${fields[@]}"$'\r' 31 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/bin/x11-unoverride-redirect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Run given command; switch override_redirect off on one new X11 window. 4 | # Also change the title to $_NET_WM_NAME (or blank), and set a fixed size hint. 5 | # Useful for working around https://github.com/QubesOS/qubes-issues/issues/2311 6 | # 7 | # Example: x11-unoverride-redirect dmenu -i 8 | 9 | import os 10 | import struct 11 | import subprocess 12 | import sys 13 | import xcffib 14 | from xcffib import xproto 15 | from xcffib.xproto import Atom, CW, PropMode 16 | 17 | XCB_ICCCM_NUM_WM_SIZE_HINTS_ELEMENTS = 18 18 | XCB_ICCCM_SIZE_HINT_P_MIN_SIZE = 1 << 4 19 | XCB_ICCCM_SIZE_HINT_P_MAX_SIZE = 1 << 5 20 | 21 | def title_property(): 22 | env = os.getenvb(b'_NET_WM_NAME', b'') 23 | return len(env), env 24 | 25 | def fixed_size_property(width, height): 26 | hint = [0] * XCB_ICCCM_NUM_WM_SIZE_HINTS_ELEMENTS 27 | hint[0] = XCB_ICCCM_SIZE_HINT_P_MIN_SIZE | XCB_ICCCM_SIZE_HINT_P_MAX_SIZE 28 | hint[5], hint[6] = hint[7], hint[8] = width, height 29 | return XCB_ICCCM_NUM_WM_SIZE_HINTS_ELEMENTS, struct.pack( 30 | f'=I{XCB_ICCCM_NUM_WM_SIZE_HINTS_ELEMENTS - 2}iI', *hint) 31 | 32 | def main(): 33 | connection = xcffib.connect() 34 | core = connection.core 35 | 36 | for s in ('_NET_WM_NAME', 'UTF8_STRING'): 37 | setattr(Atom, s, core.InternAtom(0, len(s), s).reply().atom) 38 | 39 | core.ChangeWindowAttributesChecked( 40 | connection.setup.roots[0].root, CW.EventMask, 41 | [xproto.EventMask.SubstructureNotify] 42 | ).check() 43 | 44 | window = None 45 | 46 | with subprocess.Popen(sys.argv[1:]) as command: 47 | for ev in iter(connection.wait_for_event, None): 48 | if command.poll() is not None: # command finished 49 | break 50 | 51 | if window is None: 52 | if isinstance(ev, xproto.CreateNotifyEvent) \ 53 | and ev.override_redirect: 54 | window = ev.window 55 | 56 | cookies = [ 57 | core.ChangeWindowAttributesChecked( 58 | window, CW.OverrideRedirect, [0]), 59 | ] 60 | connection.flush() 61 | 62 | cookies += [ 63 | core.ChangePropertyChecked( 64 | PropMode.Replace, window, 65 | # pylint: disable=no-member,protected-access 66 | Atom._NET_WM_NAME, Atom.UTF8_STRING, 67 | 8, *title_property()), 68 | core.ChangePropertyChecked( 69 | PropMode.Replace, window, 70 | Atom.WM_NORMAL_HINTS, Atom.WM_SIZE_HINTS, 71 | 32, *fixed_size_property(ev.width, ev.height)), 72 | ] 73 | connection.flush() 74 | elif window == ev.window: 75 | if isinstance(ev, xproto.MapNotifyEvent): 76 | if ev.sequence < cookies[0].sequence: 77 | cookies += [ 78 | core.UnmapWindowChecked(window), 79 | core.MapWindowChecked(window), 80 | ] 81 | connection.flush() 82 | break 83 | if isinstance(ev, xproto.DestroyNotifyEvent): 84 | break 85 | 86 | for c in cookies: 87 | c.check() 88 | connection.disconnect() 89 | 90 | status = command.returncode 91 | if status < 0: 92 | status = 128 - status 93 | return status 94 | 95 | 96 | if __name__ == '__main__': 97 | sys.exit(main()) 98 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/lib/tmpfiles.d/split-browser.conf: -------------------------------------------------------------------------------- 1 | d /run/split-browser - user user 2 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/share/applications/split-browser-safest.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Split Browser (TB Security Level: Safest) 3 | GenericName=Web Browser 4 | Comment=Disposable browser with persistent bookmarks and passwords 5 | Keywords=Firefox;Tor 6 | Categories=Network;WebBrowser;Security 7 | Icon=web-browser 8 | Type=Application 9 | MimeType=x-scheme-handler/https;x-scheme-handler/http 10 | Exec=split-browser --safest %u 11 | Terminal=false 12 | StartupNotify=false 13 | -------------------------------------------------------------------------------- /vm/qubes-split-browser/usr/share/applications/split-browser.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Split Browser 3 | GenericName=Web Browser 4 | Comment=Disposable browser with persistent bookmarks and passwords 5 | Keywords=Firefox;Tor 6 | Categories=Network;WebBrowser;Security 7 | Icon=web-browser 8 | Type=Application 9 | MimeType=x-scheme-handler/https;x-scheme-handler/http 10 | Exec=split-browser %u 11 | Terminal=false 12 | StartupNotify=false 13 | --------------------------------------------------------------------------------