# Bindings for normal mode
config.unbind('+')
+config.unbind('q')
+config.bind('q', 'spawn --userscript dmenu_qutebrowser')
+config.bind('Q', 'spawn --userscript qr')
config.bind('j', 'cmd-run-with-count 3 scroll down')
config.bind('k', 'cmd-run-with-count 3 scroll up')
config.bind(',M', 'hint links spawn vlc {hint-url}')
+++ /dev/null
-#!/usr/bin/env bash
-
-pngfile=$(mktemp --suffix=.png)
-trap 'rm -f "$pngfile"' EXIT
-
-qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL"
-echo ":open -t file:///$pngfile" >> "$QUTE_FIFO"
-sleep 1 # give qutebrowser time to open the file before it gets removed
+++ /dev/null
-#!/usr/bin/env python3
-
-# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-"""
-# Introduction
-
-This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
-
-
-# Installation
-
-First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
-
-
-Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
-
-
-Third, install the python module `pynacl`.
-
-
-Finally, adapt your qutebrowser config.
-You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
-Remember to replace `ABC1234` with your actual GPG key.
-
-```python
-config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
-config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
-```
-
-To manage multiple accounts you also need [rofi](https://github.com/davatorium/rofi) installed.
-
-
-# Usage
-
-If you are on a webpage with a login form, simply activate one of the configured key-bindings.
-
-The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension.
-Just provide a name of your choice and accept the request if nothing looks fishy.
-
-
-# How it works
-
-This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4].
-
-
-This script needs to store the key used to associate with your KeepassXC instance somewhere.
-Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way
-by storing the key in encrypted form using GPG.
-Therefore you need to have a public-key-pair readily set up.
-
-GPG might then ask for your private-key password whenever you query the database for login credentials.
-
-
-# TOTP
-
-This script recently received experimental TOTP support.
-To use it, you need to have working TOTP authentication within KeepassXC.
-Then call `qute-keepassxc` with the `--totp` flags.
-
-For example, I have the following line in my `config.py`:
-
-```python
-config.bind('pt', 'spawn --userscript qute-keepassxc --key ABC1234 --totp', mode='normal')
-```
-
-For now this script will simply insert the TOTP-token into the currently selected
-input field, since I have not yet found a reliable way to identify the correct field
-within all existing login forms.
-Thus you need to manually select the TOTP input field, press escape to leave input
-mode and then enter `pt` to fill in the token (or configure another key-binding for
-insert mode if you prefer that).
-
-
-[1]: https://keepassxc.org/
-[2]: https://qutebrowser.org/
-[3]: https://gnupg.org/
-[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
-[5]: https://github.com/qutebrowser/qutebrowser/blob/main/doc/userscripts.asciidoc
-[6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration
-"""
-
-import sys
-import os
-import socket
-import json
-import base64
-import subprocess
-import argparse
-
-import nacl.utils
-import nacl.public
-
-
-def parse_args():
- parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
- parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
- parser.add_argument('--totp', action='store_true',
- help="Fill in current TOTP field instead of username/password")
- parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
- help='Path to KeepassXC browser socket')
- parser.add_argument('--key', '-k', default='alice@example.com',
- help='GPG key to encrypt KeepassXC auth key with')
- parser.add_argument('--insecure', action='store_true',
- help="Do not encrypt auth key")
- return parser.parse_args()
-
-
-class KeepassError(Exception):
- def __init__(self, code, desc):
- self.code = code
- self.description = desc
-
- def __str__(self):
- return f"KeepassXC Error [{self.code}]: {self.description}"
-
-
-class KeepassXC:
- """ Wrapper around the KeepassXC socket API """
- def __init__(self, id=None, *, key, socket_path):
- self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- self.id = id
- self.socket_path = socket_path
- self.client_key = nacl.public.PrivateKey.generate()
- self.id_key = nacl.public.PrivateKey.from_seed(key)
- self.cryptobox = None
-
- def connect(self):
- if not os.path.exists(self.socket_path):
- raise KeepassError(-1, "KeepassXC Browser socket does not exists")
- self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8')
- self.sock.connect(self.socket_path)
-
- self.send_raw_msg(dict(
- action = 'change-public-keys',
- publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
- nonce = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'),
- clientID = self.client_id
- ))
-
- resp = self.recv_raw_msg()
- assert resp['action'] == 'change-public-keys'
- assert resp['success'] == 'true'
- assert resp['nonce']
- self.cryptobox = nacl.public.Box(
- self.client_key,
- nacl.public.PublicKey(base64.b64decode(resp['publicKey']))
- )
-
- def get_databasehash(self):
- self.send_msg(dict(action='get-databasehash'))
- return self.recv_msg()['hash']
-
- def lock_database(self):
- self.send_msg(dict(action='lock-database'))
- try:
- self.recv_msg()
- except KeepassError as e:
- if e.code == 1:
- return True
- raise
- return False
-
-
- def test_associate(self):
- if not self.id:
- return False
- self.send_msg(dict(
- action = 'test-associate',
- id = self.id,
- key = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
- ), triggerUnlock = 'true')
- return self.recv_msg()['success'] == 'true'
-
- def associate(self):
- self.send_msg(dict(
- action = 'associate',
- key = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
- idKey = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
- ))
- resp = self.recv_msg()
- self.id = resp['id']
-
- def get_logins(self, url):
- self.send_msg(dict(
- action = 'get-logins',
- url = url,
- keys = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }]
- ))
- return self.recv_msg()['entries']
-
- def get_totp(self, uuid):
- self.send_msg(dict(
- action = 'get-totp',
- uuid = uuid
- ))
- response = self.recv_msg()
- if response['success'] != 'true' or not response['totp']:
- return None
- return response['totp']
-
- def send_raw_msg(self, msg):
- self.sock.send( json.dumps(msg).encode('utf-8') )
-
- def recv_raw_msg(self):
- return json.loads( self.sock.recv(4096).decode('utf-8') )
-
- def send_msg(self, msg, **extra):
- nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE)
- self.send_raw_msg(dict(
- action = msg['action'],
- message = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'),
- nonce = base64.b64encode(nonce).decode('utf-8'),
- clientID = self.client_id,
- **extra
- ))
-
- def recv_msg(self):
- resp = self.recv_raw_msg()
- if 'error' in resp:
- raise KeepassError(resp['errorCode'], resp['error'])
- assert resp['action']
- return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8'))
-
-
-
-class SecretKeyStore:
- def __init__(self, gpgkey, insecure):
- self.gpgkey = gpgkey
- self.insecure = insecure
- if self.insecure:
- self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key')
- else:
- self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg')
-
- def load(self):
- "Load existing association key from file"
- if self.insecure:
- jsondata = open(self.path, 'r').read()
- else:
- jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8')
- data = json.loads(jsondata)
- self.id = data['id']
- self.key = base64.b64decode(data['key'])
-
- def create(self):
- "Create new association key"
- self.key = nacl.utils.random(32)
- self.id = None
-
- def store(self, id):
- "Store newly created association key in file"
- self.id = id
- jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')})
- if self.insecure:
- open(self.path, "w").write(jsondata)
- else:
- subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True)
-
-
-def qute(cmd):
- with open(os.environ['QUTE_FIFO'], 'w') as fifo:
- fifo.write(cmd)
- fifo.write('\n')
- fifo.flush()
-
-def error(msg):
- print(msg, file=sys.stderr)
- qute('message-error "{}"'.format(msg))
-
-
-def connect_to_keepassxc(args):
- if not args.insecure and not args.key:
- error("Missing GPG key to use for auth key encryption")
- return
- keystore = SecretKeyStore(args.key, args.insecure)
- if os.path.isfile(keystore.path):
- keystore.load()
- kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket)
- kp.connect()
- if not kp.test_associate():
- error('No KeepassXC association')
- return None
- else:
- keystore.create()
- kp = KeepassXC(key=keystore.key, socket_path=args.socket)
- kp.connect()
- kp.associate()
- if not kp.test_associate():
- error('No KeepassXC association')
- return None
- keystore.store(kp.id)
- return kp
-
-
-def select_account(creds):
- try:
- if len(creds) == 1:
- return creds[0]
- idx = subprocess.check_output(
- ['rofi', '-dmenu', '-format', 'i', '-matching', 'fuzzy',
- '-p', 'Search',
- '-mesg', '<b>qute-keepassxc</b>: select an account, please!'],
- input=b"\n".join(c['login'].encode('utf-8') for c in creds)
- )
- idx = int(idx)
- if idx < 0:
- return None
- return creds[idx]
- except subprocess.CalledProcessError:
- return None
- except FileNotFoundError:
- error("rofi not found. Please install rofi to select from multiple credentials")
- return creds[0]
- except Exception as e:
- error(f"Error while picking account: {e}")
- return None
-
-
-def make_js_code(username, password):
- return ' '.join("""
- function isVisible(elem) {
- var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
-
- if (style.getPropertyValue("visibility") !== "visible" ||
- style.getPropertyValue("display") === "none" ||
- style.getPropertyValue("opacity") === "0") {
- return false;
- }
-
- return elem.offsetWidth > 0 && elem.offsetHeight > 0;
- };
-
- function hasPasswordField(form) {
- var inputs = form.getElementsByTagName("input");
- for (var j = 0; j < inputs.length; j++) {
- var input = inputs[j];
- if (input.type === "password") {
- return true;
- }
- }
- return false;
- };
-
- function loadData2Form (form) {
- var inputs = form.getElementsByTagName("input");
- for (var j = 0; j < inputs.length; j++) {
- var input = inputs[j];
- if (isVisible(input) && (input.type === "text" || input.type === "email")) {
- input.focus();
- input.value = %s;
- input.dispatchEvent(new Event('input', { 'bubbles': true }));
- input.dispatchEvent(new Event('change', { 'bubbles': true }));
- input.blur();
- }
- if (input.type === "password") {
- input.focus();
- input.value = %s;
- input.dispatchEvent(new Event('input', { 'bubbles': true }));
- input.dispatchEvent(new Event('change', { 'bubbles': true }));
- input.blur();
- }
- }
- };
-
- function fillFirstForm() {
- var forms = document.getElementsByTagName("form");
- for (i = 0; i < forms.length; i++) {
- if (hasPasswordField(forms[i])) {
- loadData2Form(forms[i]);
- return;
- }
- }
- alert("No Credentials Form found");
- };
-
- fillFirstForm()
- """.splitlines()) % (json.dumps(username), json.dumps(password))
-
-
-def make_js_totp_code(totp):
- return ' '.join("""
- (function () {
- var input = document.activeElement;
- if (!input || input.tagName !== "INPUT") {
- alert("No TOTP input field selected");
- return;
- }
- input.value = %s;
- input.dispatchEvent(new Event('input', { 'bubbles': true }));
- input.dispatchEvent(new Event('change', { 'bubbles': true }));
- })();
- """.splitlines()) % (json.dumps(totp),)
-
-
-def main():
- if 'QUTE_FIFO' not in os.environ:
- print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
- sys.exit(-1)
-
- try:
- args = parse_args()
- assert args.url, "Missing URL"
- kp = connect_to_keepassxc(args)
- if not kp:
- error('Could not connect to KeepassXC')
- return
- creds = kp.get_logins(args.url)
- if not creds:
- error('No credentials found')
- return
- cred = select_account(creds)
- if not cred:
- error('No credentials selected')
- return
- if args.totp:
- uuid = cred['uuid']
- totp = kp.get_totp(uuid)
- if not totp:
- error('No TOTP key found')
- return
- qute('jseval -q ' + make_js_totp_code(totp))
- else:
- name, pw = cred['login'], cred['password']
- if name and pw:
- qute('jseval -q ' + make_js_code(name, pw))
- except Exception as e:
- error(str(e))
-
-
-if __name__ == '__main__':
- main()
-
+++ /dev/null
-#!/usr/bin/env bash
-#
-# Behavior:
-# Userscript for qutebrowser which views the current web page in mpv using
-# sensible mpv-flags. While viewing the page in MPV, all <video>, <embed>,
-# and <object> tags in the original page are temporarily removed. Clicking on
-# such a removed video restores the respective video.
-#
-# In order to use this script, just start it using `spawn --userscript` from
-# qutebrowser. I recommend using an alias, e.g. put this in the
-# [alias]-section of qutebrowser.conf:
-#
-# mpv = spawn --userscript /path/to/view_in_mpv
-#
-# Background:
-# Most of my machines are too slow to play youtube videos using html5, but
-# they work fine in mpv (and mpv has further advantages like video scaling,
-# etc). Of course, I don't want the video to be played (or even to be
-# downloaded) twice — in MPV and in qwebkit. So I often close the tab after
-# opening it in mpv. However, I actually want to keep the rest of the page
-# (comments and video suggestions), i.e. only the videos should disappear
-# when mpv is started. And that's precisely what the present script does.
-#
-# Thorsten Wißmann, 2015 (thorsten` on Libera Chat)
-# Any feedback is welcome!
-
-set -e
-
-if [ -z "$QUTE_FIFO" ] ; then
- cat 1>&2 <<EOF
-Error: $0 can not be run as a standalone script.
-
-It is a qutebrowser userscript. In order to use it, call it using
-'spawn --userscript' as described in qute://help/userscripts.html
-EOF
- exit 1
-fi
-
-msg() {
- local cmd="$1"
- shift
- local msg="$*"
- if [ -z "$QUTE_FIFO" ] ; then
- echo "$cmd: $msg" >&2
- else
- echo "message-$cmd '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
- fi
-}
-
-MPV_COMMAND=${MPV_COMMAND:-mpv}
-# Warning: spaces in single flags are not supported
-MPV_FLAGS=${MPV_FLAGS:- --force-window --quiet --keep-open=yes --ytdl}
-IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
-
-js() {
-cat <<EOF
-
- function descendantOfTagName(child, ancestorTagName) {
- // tells whether child has some (proper) ancestor
- // with the tag name ancestorTagName
- while (child.parentNode != null) {
- child = child.parentNode;
- if (typeof child.tagName === 'undefined') break;
- if (child.tagName.toUpperCase() == ancestorTagName.toUpperCase()) {
- return true;
- }
- }
- return false;
- }
-
- var App = {};
-
- var all_videos = [];
- all_videos.push.apply(all_videos, document.getElementsByTagName("video"));
- all_videos.push.apply(all_videos, document.getElementsByTagName("object"));
- all_videos.push.apply(all_videos, document.getElementsByTagName("embed"));
- App.backup_videos = Array();
- App.all_replacements = Array();
- for (i = 0; i < all_videos.length; i++) {
- var video = all_videos[i];
- if (descendantOfTagName(video, "object")) {
- // skip tags that are contained in an object, because we hide
- // the object anyway.
- continue;
- }
- var replacement = document.createElement("div");
- replacement.innerHTML = "
- <p style=\\"margin-bottom: 0.5em\\">
- Opening page with:
- <span style=\\"font-family: monospace;\\">${video_command[*]}</span>
- </p>
- <p>
- In order to restore this particular video
- <a style=\\"font-weight: bold;
- color: white;
- background: transparent;
- cursor: pointer;
- \\"
- onClick=\\"restore_video(this, " + i + ");\\"
- >click here</a>.
- </p>
- ";
- replacement.style.position = "relative";
- replacement.style.zIndex = "100003000000";
- replacement.style.fontSize = "1rem";
- replacement.style.textAlign = "center";
- replacement.style.verticalAlign = "middle";
- replacement.style.height = "100%";
- replacement.style.background = "#101010";
- replacement.style.color = "white";
- replacement.style.border = "4px dashed #545454";
- replacement.style.padding = "2em";
- replacement.style.margin = "auto";
- App.all_replacements[i] = replacement;
- App.backup_videos[i] = video;
- video.parentNode.replaceChild(replacement, video);
- }
-
- function restore_video(obj, index) {
- obj = App.all_replacements[index];
- video = App.backup_videos[index];
- obj.parentNode.replaceChild(video, obj);
- }
-
- /** force repainting the video, thanks to:
- * http://web.archive.org/web/20151029064649/https://martinwolf.org/2014/06/10/force-repaint-of-an-element-with-javascript/
- */
- var siteHeader = document.getElementById('header');
- siteHeader.style.display='none';
- siteHeader.offsetHeight; // no need to store this anywhere, the reference is enough
- siteHeader.style.display='block';
-
-EOF
-}
-
-printjs() {
- js | sed 's,//.*$,,' | tr '\n' ' '
-}
-echo "jseval -q -w main $(printjs)" >> "$QUTE_FIFO"
-
-msg info "Opening $QUTE_URL with mpv"
-"${video_command[@]}" "$@" "$QUTE_URL"
--- /dev/null
+#!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Zach-Button <zachrey.button@gmail.com>
+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Pipes history, quickmarks, and URL into dmenu.
+#
+# If run from qutebrowser as a userscript, it runs :open on the URL
+# If not, it opens a new qutebrowser window at the URL
+#
+# Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
+# :bind o spawn --userscript dmenu_qutebrowser
+#
+# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
+# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
+#
+# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
+# Default keys Keys with this script
+# O <Mod4>o
+# o o
+# go o<Tab>
+# gO gC, then o<Tab>
+# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
+#
+
+
+[ -z "$QUTE_URL" ] && QUTE_URL='https://duckduckgo.com'
+
+url=$(printf "%s\n%s" "$QUTE_URL" "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')" | cat "$QUTE_CONFIG_DIR/quickmarks" - | fuzzel -d -l 15 -w 150 -p qutebrowser)
+url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url")
+
+[ -z "${url// }" ] && exit
+
+echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
--- /dev/null
+#!/usr/bin/env bash
+
+pngfile=$(mktemp --suffix=.png)
+trap 'rm -f "$pngfile"' EXIT
+
+qrencode -t PNG -o "$pngfile" -s 10 "$QUTE_URL"
+echo ":open -t file:///$pngfile" >> "$QUTE_FIFO"
+sleep 1 # give qutebrowser time to open the file before it gets removed
--- /dev/null
+#!/usr/bin/env python3
+
+# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""
+# Introduction
+
+This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
+
+
+# Installation
+
+First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
+
+
+Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
+
+
+Third, install the python module `pynacl`.
+
+
+Finally, adapt your qutebrowser config.
+You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
+Remember to replace `ABC1234` with your actual GPG key.
+
+```python
+config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
+config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
+```
+
+To manage multiple accounts you also need [rofi](https://github.com/davatorium/rofi) installed.
+
+
+# Usage
+
+If you are on a webpage with a login form, simply activate one of the configured key-bindings.
+
+The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension.
+Just provide a name of your choice and accept the request if nothing looks fishy.
+
+
+# How it works
+
+This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4].
+
+
+This script needs to store the key used to associate with your KeepassXC instance somewhere.
+Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way
+by storing the key in encrypted form using GPG.
+Therefore you need to have a public-key-pair readily set up.
+
+GPG might then ask for your private-key password whenever you query the database for login credentials.
+
+
+# TOTP
+
+This script recently received experimental TOTP support.
+To use it, you need to have working TOTP authentication within KeepassXC.
+Then call `qute-keepassxc` with the `--totp` flags.
+
+For example, I have the following line in my `config.py`:
+
+```python
+config.bind('pt', 'spawn --userscript qute-keepassxc --key ABC1234 --totp', mode='normal')
+```
+
+For now this script will simply insert the TOTP-token into the currently selected
+input field, since I have not yet found a reliable way to identify the correct field
+within all existing login forms.
+Thus you need to manually select the TOTP input field, press escape to leave input
+mode and then enter `pt` to fill in the token (or configure another key-binding for
+insert mode if you prefer that).
+
+
+[1]: https://keepassxc.org/
+[2]: https://qutebrowser.org/
+[3]: https://gnupg.org/
+[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
+[5]: https://github.com/qutebrowser/qutebrowser/blob/main/doc/userscripts.asciidoc
+[6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration
+"""
+
+import sys
+import os
+import socket
+import json
+import base64
+import subprocess
+import argparse
+
+import nacl.utils
+import nacl.public
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
+ parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
+ parser.add_argument('--totp', action='store_true',
+ help="Fill in current TOTP field instead of username/password")
+ parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
+ help='Path to KeepassXC browser socket')
+ parser.add_argument('--key', '-k', default='alice@example.com',
+ help='GPG key to encrypt KeepassXC auth key with')
+ parser.add_argument('--insecure', action='store_true',
+ help="Do not encrypt auth key")
+ return parser.parse_args()
+
+
+class KeepassError(Exception):
+ def __init__(self, code, desc):
+ self.code = code
+ self.description = desc
+
+ def __str__(self):
+ return f"KeepassXC Error [{self.code}]: {self.description}"
+
+
+class KeepassXC:
+ """ Wrapper around the KeepassXC socket API """
+ def __init__(self, id=None, *, key, socket_path):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.id = id
+ self.socket_path = socket_path
+ self.client_key = nacl.public.PrivateKey.generate()
+ self.id_key = nacl.public.PrivateKey.from_seed(key)
+ self.cryptobox = None
+
+ def connect(self):
+ if not os.path.exists(self.socket_path):
+ raise KeepassError(-1, "KeepassXC Browser socket does not exists")
+ self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8')
+ self.sock.connect(self.socket_path)
+
+ self.send_raw_msg(dict(
+ action = 'change-public-keys',
+ publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+ nonce = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'),
+ clientID = self.client_id
+ ))
+
+ resp = self.recv_raw_msg()
+ assert resp['action'] == 'change-public-keys'
+ assert resp['success'] == 'true'
+ assert resp['nonce']
+ self.cryptobox = nacl.public.Box(
+ self.client_key,
+ nacl.public.PublicKey(base64.b64decode(resp['publicKey']))
+ )
+
+ def get_databasehash(self):
+ self.send_msg(dict(action='get-databasehash'))
+ return self.recv_msg()['hash']
+
+ def lock_database(self):
+ self.send_msg(dict(action='lock-database'))
+ try:
+ self.recv_msg()
+ except KeepassError as e:
+ if e.code == 1:
+ return True
+ raise
+ return False
+
+
+ def test_associate(self):
+ if not self.id:
+ return False
+ self.send_msg(dict(
+ action = 'test-associate',
+ id = self.id,
+ key = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+ ), triggerUnlock = 'true')
+ return self.recv_msg()['success'] == 'true'
+
+ def associate(self):
+ self.send_msg(dict(
+ action = 'associate',
+ key = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+ idKey = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+ ))
+ resp = self.recv_msg()
+ self.id = resp['id']
+
+ def get_logins(self, url):
+ self.send_msg(dict(
+ action = 'get-logins',
+ url = url,
+ keys = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }]
+ ))
+ return self.recv_msg()['entries']
+
+ def get_totp(self, uuid):
+ self.send_msg(dict(
+ action = 'get-totp',
+ uuid = uuid
+ ))
+ response = self.recv_msg()
+ if response['success'] != 'true' or not response['totp']:
+ return None
+ return response['totp']
+
+ def send_raw_msg(self, msg):
+ self.sock.send( json.dumps(msg).encode('utf-8') )
+
+ def recv_raw_msg(self):
+ return json.loads( self.sock.recv(4096).decode('utf-8') )
+
+ def send_msg(self, msg, **extra):
+ nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE)
+ self.send_raw_msg(dict(
+ action = msg['action'],
+ message = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'),
+ nonce = base64.b64encode(nonce).decode('utf-8'),
+ clientID = self.client_id,
+ **extra
+ ))
+
+ def recv_msg(self):
+ resp = self.recv_raw_msg()
+ if 'error' in resp:
+ raise KeepassError(resp['errorCode'], resp['error'])
+ assert resp['action']
+ return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8'))
+
+
+
+class SecretKeyStore:
+ def __init__(self, gpgkey, insecure):
+ self.gpgkey = gpgkey
+ self.insecure = insecure
+ if self.insecure:
+ self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key')
+ else:
+ self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg')
+
+ def load(self):
+ "Load existing association key from file"
+ if self.insecure:
+ jsondata = open(self.path, 'r').read()
+ else:
+ jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8')
+ data = json.loads(jsondata)
+ self.id = data['id']
+ self.key = base64.b64decode(data['key'])
+
+ def create(self):
+ "Create new association key"
+ self.key = nacl.utils.random(32)
+ self.id = None
+
+ def store(self, id):
+ "Store newly created association key in file"
+ self.id = id
+ jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')})
+ if self.insecure:
+ open(self.path, "w").write(jsondata)
+ else:
+ subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True)
+
+
+def qute(cmd):
+ with open(os.environ['QUTE_FIFO'], 'w') as fifo:
+ fifo.write(cmd)
+ fifo.write('\n')
+ fifo.flush()
+
+def error(msg):
+ print(msg, file=sys.stderr)
+ qute('message-error "{}"'.format(msg))
+
+
+def connect_to_keepassxc(args):
+ if not args.insecure and not args.key:
+ error("Missing GPG key to use for auth key encryption")
+ return
+ keystore = SecretKeyStore(args.key, args.insecure)
+ if os.path.isfile(keystore.path):
+ keystore.load()
+ kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket)
+ kp.connect()
+ if not kp.test_associate():
+ error('No KeepassXC association')
+ return None
+ else:
+ keystore.create()
+ kp = KeepassXC(key=keystore.key, socket_path=args.socket)
+ kp.connect()
+ kp.associate()
+ if not kp.test_associate():
+ error('No KeepassXC association')
+ return None
+ keystore.store(kp.id)
+ return kp
+
+
+def select_account(creds):
+ try:
+ if len(creds) == 1:
+ return creds[0]
+ idx = subprocess.check_output(
+ ['rofi', '-dmenu', '-format', 'i', '-matching', 'fuzzy',
+ '-p', 'Search',
+ '-mesg', '<b>qute-keepassxc</b>: select an account, please!'],
+ input=b"\n".join(c['login'].encode('utf-8') for c in creds)
+ )
+ idx = int(idx)
+ if idx < 0:
+ return None
+ return creds[idx]
+ except subprocess.CalledProcessError:
+ return None
+ except FileNotFoundError:
+ error("rofi not found. Please install rofi to select from multiple credentials")
+ return creds[0]
+ except Exception as e:
+ error(f"Error while picking account: {e}")
+ return None
+
+
+def make_js_code(username, password):
+ return ' '.join("""
+ function isVisible(elem) {
+ var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
+
+ if (style.getPropertyValue("visibility") !== "visible" ||
+ style.getPropertyValue("display") === "none" ||
+ style.getPropertyValue("opacity") === "0") {
+ return false;
+ }
+
+ return elem.offsetWidth > 0 && elem.offsetHeight > 0;
+ };
+
+ function hasPasswordField(form) {
+ var inputs = form.getElementsByTagName("input");
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (input.type === "password") {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ function loadData2Form (form) {
+ var inputs = form.getElementsByTagName("input");
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (isVisible(input) && (input.type === "text" || input.type === "email")) {
+ input.focus();
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ input.blur();
+ }
+ if (input.type === "password") {
+ input.focus();
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ input.blur();
+ }
+ }
+ };
+
+ function fillFirstForm() {
+ var forms = document.getElementsByTagName("form");
+ for (i = 0; i < forms.length; i++) {
+ if (hasPasswordField(forms[i])) {
+ loadData2Form(forms[i]);
+ return;
+ }
+ }
+ alert("No Credentials Form found");
+ };
+
+ fillFirstForm()
+ """.splitlines()) % (json.dumps(username), json.dumps(password))
+
+
+def make_js_totp_code(totp):
+ return ' '.join("""
+ (function () {
+ var input = document.activeElement;
+ if (!input || input.tagName !== "INPUT") {
+ alert("No TOTP input field selected");
+ return;
+ }
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ })();
+ """.splitlines()) % (json.dumps(totp),)
+
+
+def main():
+ if 'QUTE_FIFO' not in os.environ:
+ print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
+ sys.exit(-1)
+
+ try:
+ args = parse_args()
+ assert args.url, "Missing URL"
+ kp = connect_to_keepassxc(args)
+ if not kp:
+ error('Could not connect to KeepassXC')
+ return
+ creds = kp.get_logins(args.url)
+ if not creds:
+ error('No credentials found')
+ return
+ cred = select_account(creds)
+ if not cred:
+ error('No credentials selected')
+ return
+ if args.totp:
+ uuid = cred['uuid']
+ totp = kp.get_totp(uuid)
+ if not totp:
+ error('No TOTP key found')
+ return
+ qute('jseval -q ' + make_js_totp_code(totp))
+ else:
+ name, pw = cred['login'], cred['password']
+ if name and pw:
+ qute('jseval -q ' + make_js_code(name, pw))
+ except Exception as e:
+ error(str(e))
+
+
+if __name__ == '__main__':
+ main()
+
--- /dev/null
+#!/usr/bin/env bash
+# Handle open -s && open -t with bemenu
+
+#:bind o spawn --userscript /path/to/userscripts/qutedmenu open
+#:bind O spawn --userscript /path/to/userscripts/qutedmenu tab
+
+# If you would like to set a custom colorscheme/font use these dirs.
+# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
+
+
+readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
+readonly optsfile=$confdir/dmenu/bemenucolors
+
+create_menu() {
+ # Check quickmarks
+ while read -r url; do
+ printf -- '%s\n' "$url"
+ done < "$QUTE_CONFIG_DIR"/quickmarks
+
+ # Next bookmarks
+ while read -r url _; do
+ printf -- '%s\n' "$url"
+ done < "$QUTE_CONFIG_DIR"/bookmarks/urls
+
+ # Finally history
+ printf -- '%s\n' "$(sqlite3 -separator ' ' "$QUTE_DATA_DIR/history.sqlite" 'select title, url from CompletionHistory')"
+ }
+
+get_selection() {
+ opts+=(-p qutebrowser)
+ create_menu | fuzzel -d -l 10 "${opts[@]}"
+ #create_menu | dmenu -l 10 "${opts[@]}"
+ #create_menu | bemenu -l 10 "${opts[@]}"
+}
+
+# Main
+# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
+[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
+
+[[ -n $font ]] && opts+=(-fn "$font")
+
+# shellcheck source=/dev/null
+[[ -s $optsfile ]] && source "$optsfile"
+
+url=$(get_selection)
+url=${url/*http/http}
+
+# If no selection is made, exit (escape pressed, e.g.)
+[[ -z $url ]] && exit 0
+
+case $1 in
+ open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
+ tab) printf '%s' "open -t $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
+ window) printf '%s' "open -w $url" >> "$QUTE_FIFO" || qutebrowser "$url --target window" ;;
+ private) printf '%s' "open -p $url" >> "$QUTE_FIFO" || qutebrowser "$url --target private-window" ;;
+esac
--- /dev/null
+#!/usr/bin/env bash
+#
+# Behavior:
+# Userscript for qutebrowser which views the current web page in mpv using
+# sensible mpv-flags. While viewing the page in MPV, all <video>, <embed>,
+# and <object> tags in the original page are temporarily removed. Clicking on
+# such a removed video restores the respective video.
+#
+# In order to use this script, just start it using `spawn --userscript` from
+# qutebrowser. I recommend using an alias, e.g. put this in the
+# [alias]-section of qutebrowser.conf:
+#
+# mpv = spawn --userscript /path/to/view_in_mpv
+#
+# Background:
+# Most of my machines are too slow to play youtube videos using html5, but
+# they work fine in mpv (and mpv has further advantages like video scaling,
+# etc). Of course, I don't want the video to be played (or even to be
+# downloaded) twice — in MPV and in qwebkit. So I often close the tab after
+# opening it in mpv. However, I actually want to keep the rest of the page
+# (comments and video suggestions), i.e. only the videos should disappear
+# when mpv is started. And that's precisely what the present script does.
+#
+# Thorsten Wißmann, 2015 (thorsten` on Libera Chat)
+# Any feedback is welcome!
+
+set -e
+
+if [ -z "$QUTE_FIFO" ] ; then
+ cat 1>&2 <<EOF
+Error: $0 can not be run as a standalone script.
+
+It is a qutebrowser userscript. In order to use it, call it using
+'spawn --userscript' as described in qute://help/userscripts.html
+EOF
+ exit 1
+fi
+
+msg() {
+ local cmd="$1"
+ shift
+ local msg="$*"
+ if [ -z "$QUTE_FIFO" ] ; then
+ echo "$cmd: $msg" >&2
+ else
+ echo "message-$cmd '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
+ fi
+}
+
+MPV_COMMAND=${MPV_COMMAND:-mpv}
+# Warning: spaces in single flags are not supported
+MPV_FLAGS=${MPV_FLAGS:- --force-window --quiet --keep-open=yes --ytdl}
+IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
+
+js() {
+cat <<EOF
+
+ function descendantOfTagName(child, ancestorTagName) {
+ // tells whether child has some (proper) ancestor
+ // with the tag name ancestorTagName
+ while (child.parentNode != null) {
+ child = child.parentNode;
+ if (typeof child.tagName === 'undefined') break;
+ if (child.tagName.toUpperCase() == ancestorTagName.toUpperCase()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var App = {};
+
+ var all_videos = [];
+ all_videos.push.apply(all_videos, document.getElementsByTagName("video"));
+ all_videos.push.apply(all_videos, document.getElementsByTagName("object"));
+ all_videos.push.apply(all_videos, document.getElementsByTagName("embed"));
+ App.backup_videos = Array();
+ App.all_replacements = Array();
+ for (i = 0; i < all_videos.length; i++) {
+ var video = all_videos[i];
+ if (descendantOfTagName(video, "object")) {
+ // skip tags that are contained in an object, because we hide
+ // the object anyway.
+ continue;
+ }
+ var replacement = document.createElement("div");
+ replacement.innerHTML = "
+ <p style=\\"margin-bottom: 0.5em\\">
+ Opening page with:
+ <span style=\\"font-family: monospace;\\">${video_command[*]}</span>
+ </p>
+ <p>
+ In order to restore this particular video
+ <a style=\\"font-weight: bold;
+ color: white;
+ background: transparent;
+ cursor: pointer;
+ \\"
+ onClick=\\"restore_video(this, " + i + ");\\"
+ >click here</a>.
+ </p>
+ ";
+ replacement.style.position = "relative";
+ replacement.style.zIndex = "100003000000";
+ replacement.style.fontSize = "1rem";
+ replacement.style.textAlign = "center";
+ replacement.style.verticalAlign = "middle";
+ replacement.style.height = "100%";
+ replacement.style.background = "#101010";
+ replacement.style.color = "white";
+ replacement.style.border = "4px dashed #545454";
+ replacement.style.padding = "2em";
+ replacement.style.margin = "auto";
+ App.all_replacements[i] = replacement;
+ App.backup_videos[i] = video;
+ video.parentNode.replaceChild(replacement, video);
+ }
+
+ function restore_video(obj, index) {
+ obj = App.all_replacements[index];
+ video = App.backup_videos[index];
+ obj.parentNode.replaceChild(video, obj);
+ }
+
+ /** force repainting the video, thanks to:
+ * http://web.archive.org/web/20151029064649/https://martinwolf.org/2014/06/10/force-repaint-of-an-element-with-javascript/
+ */
+ var siteHeader = document.getElementById('header');
+ siteHeader.style.display='none';
+ siteHeader.offsetHeight; // no need to store this anywhere, the reference is enough
+ siteHeader.style.display='block';
+
+EOF
+}
+
+printjs() {
+ js | sed 's,//.*$,,' | tr '\n' ' '
+}
+echo "jseval -q -w main $(printjs)" >> "$QUTE_FIFO"
+
+msg info "Opening $QUTE_URL with mpv"
+"${video_command[@]}" "$@" "$QUTE_URL"