From 301ce5302999f63d9c49661bbec0358a6b17da3c Mon Sep 17 00:00:00 2001 From: Jeff LANCE Date: Thu, 1 Jul 2021 21:42:00 +0200 Subject: [PATCH] update conf --- conf.d/qutebrowser/config.py | 31 +- conf.d/qutebrowser/userscripts/password_fill | 385 +++++++++++++++++++ conf.d/qutebrowser/userscripts/qute-pass | 4 +- 3 files changed, 405 insertions(+), 15 deletions(-) create mode 100755 conf.d/qutebrowser/userscripts/password_fill diff --git a/conf.d/qutebrowser/config.py b/conf.d/qutebrowser/config.py index ee2969c..30c1b13 100644 --- a/conf.d/qutebrowser/config.py +++ b/conf.d/qutebrowser/config.py @@ -1598,38 +1598,43 @@ config.unbind('m', mode='normal') config.unbind('M', mode='normal') config.unbind('o', mode='normal') -## Bindings for normal mode config.bind(';;', 'mode-leave', mode='insert') -# config.bind('', 'clear-keychain ;; search ;; fullscreen --leave') config.bind('', 'reload') -config.bind('', 'home') - config.bind('', 'back') config.bind('', 'forward') +config.bind('', 'home') config.bind('', 'set-cmd-text -s :open') config.bind('', 'reload') config.bind('', 'stop') config.bind('', 'tab-close') +config.bind('', 'tab-next') +config.bind('', 'tab-prev') bind_chained('', 'message-info "Reload config"', 'config-source') config.bind('', 'close') +config.bind('', 'tab-move +') +config.bind('', 'tab-move -') config.bind('b', 'set-cmd-text -s :quickmark-load') config.bind('B', 'set-cmd-text -s :quickmark-load -t') config.bind('m', 'quickmark-save') -# qute-pass +# password_fill config.bind('Qa', "spawn --userscript \ - ~/.config/qutebrowser/userscripts/qute-pass -p \ - '/home/jeff/Private/.password-store/'") -config.bind('Qu', "spawn --userscript \ - ~/.config/qutebrowser/userscripts/qute-pass -p \ - '/home/jeff/Private/.password-store/' --username-only") -config.bind('Qp', "spawn --userscript \ - ~/.config/qutebrowser/userscripts/qute-pass -p \ - '/home/jeff/Private/.password-store/' --password-only") + ~/.config/qutebrowser/userscripts/password_fill") + +# # qute-pass +# config.bind('Qa', "spawn --userscript \ +# ~/.config/qutebrowser/userscripts/qute-pass -p \ +# '/home/jeff/Private/.password-store/'") +# config.bind('Qu', "spawn --userscript \ +# ~/.config/qutebrowser/userscripts/qute-pass -p \ +# '/home/jeff/Private/.password-store/' --username-only") +# config.bind('Qp', "spawn --userscript \ +# ~/.config/qutebrowser/userscripts/qute-pass -p \ +# '/home/jeff/Private/.password-store/' --password-only") # config.bind('', 'stop') # config.bind("'", 'enter-mode jump_mark') diff --git a/conf.d/qutebrowser/userscripts/password_fill b/conf.d/qutebrowser/userscripts/password_fill new file mode 100755 index 0000000..c46253d --- /dev/null +++ b/conf.d/qutebrowser/userscripts/password_fill @@ -0,0 +1,385 @@ +#!/usr/bin/env bash +help() { + blink=$'\e[1;31m' reset=$'\e[0m' +cat < +In case of questions or suggestions, do not hesitate to send me an E-Mail or to +directly ask me via IRC (nickname thorsten\`) in #qutebrowser on Libera Chat. + + $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset + WARNING: the passwords are stored in qutebrowser's + debug log reachable via the url qute://log + $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset + +Usage: run as a userscript form qutebrowser, e.g.: + spawn --userscript ~/.config/qutebrowser/password_fill + +Pass backend: (see also passwordstore.org) + This script expects pass to store the credentials of each page in an extra + file, where the filename (or filepath) contains the domain of the respective + page. The first line of the file must contain the password, the login name + must be contained in a later line beginning with "user:", "login:", or + "username:" (configurable by the user_pattern variable). + +Behavior: + It will try to find a username/password entry in the configured backend + (currently only pass) for the current website and will load that pair of + username and password to any form on the current page that has some password + entry field. If multiple entries are found, a zenity menu is offered. + + If no entry is found, then it crops subdomains from the url if at least one + entry is found in the backend. (In that case, it always shows a menu) + +Configuration: + This script loads the bash script ~/.config/qutebrowser/password_fill_rc (if + it exists), so you can change any configuration variable and overwrite any + function you like. + +EOF +} + +set -o errexit +set -o pipefail +shopt -s nocasematch # make regexp matching in bash case insensitive + +if [ -z "$QUTE_FIFO" ] ; then + help + exit +fi + +error() { + local msg="$*" + echo "message-error '${msg//\'/\\\'}'" >> "$QUTE_FIFO" +} +msg() { + local msg="$*" + echo "message-info '${msg//\'/\\\'}'" >> "$QUTE_FIFO" +} +die() { + error "$*" + exit 0 +} + +javascript_escape() { + # print the first argument in an escaped way, such that it can safely + # be used within javascripts double quotes + # shellcheck disable=SC2001 + sed "s,[\\\\'\"],\\\\&,g" <<< "$1" +} + +# ======================================================= # +# CONFIGURATION +# ======================================================= # +# The configuration file is per default located in +# ~/.config/qutebrowser/password_fill_rc and is a bash script that is loaded +# later in the present script. So basically you can replace all of the +# following definitions and make them fit your needs. + +# The following simplifies a URL to the domain (e.g. "wiki.qutebrowser.org") +# which is later used to search the correct entries in the password backend. If +# you e.g. don't want the "www." to be removed or if you want to distinguish +# between different paths on the same domain. + +simplify_url() { + simple_url="${1##*://}" # remove protocol specification + simple_url="${simple_url%%\?*}" # remove GET parameters + simple_url="${simple_url%%/*}" # remove directory path + simple_url="${simple_url%:*}" # remove port + simple_url="${simple_url##www.}" # remove www. subdomain +} + +# no_entries_found() is called if the first query_entries() call did not find +# any matching entries. Multiple implementations are possible: +# The easiest behavior is to quit: +#no_entries_found() { +# if [ 0 -eq "${#files[@]}" ] ; then +# die "No entry found for »$simple_url«" +# fi +#} +# But you could also fill the files array with all entries from your pass db +# if the first db query did not find anything +# no_entries_found() { +# if [ 0 -eq "${#files[@]}" ] ; then +# query_entries "" +# if [ 0 -eq "${#files[@]}" ] ; then +# die "No entry found for »$simple_url«" +# fi +# fi +# } + +# Another behavior is to drop another level of subdomains until search hits +# are found: +no_entries_found() { + while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do + # shellcheck disable=SC2001 + shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") + if [ "$shorter_simple_url" = "$simple_url" ] ; then + # if no dot, then even remove the top level domain + simple_url="" + query_entries "$simple_url" + break + fi + simple_url="$shorter_simple_url" + query_entries "$simple_url" + #die "No entry found for »$simple_url«" + # enforce menu if we do "fuzzy" matching + menu_if_one_entry=1 + done + if [ 0 -eq "${#files[@]}" ] ; then + die "No entry found for »$simple_url«" + fi +} + +# Backend implementations tell, how the actual password store is accessed. +# Right now, there is only one fully functional password backend, namely for +# the program "pass". +# A password backend consists of three actions: +# - init() initializes backend-specific things and does sanity checks. +# - query_entries() is called with a simplified url and is expected to fill +# the bash array $files with the names of matching password entries. There +# are no requirements how these names should look like. +# - open_entry() is called with some specific entry of the $files array and is +# expected to write the username of that entry to the $username variable and +# the corresponding password to $password + +reset_backend() { + init() { true ; } + query_entries() { true ; } + open_entry() { true ; } +} + +# choose_entry() is expected to choose one entry from the array $files and +# write it to the variable $file. +choose_entry() { + choose_entry_zenity +} + +# The default implementation chooses a random entry from the array. So if there +# are multiple matching entries, multiple calls to this userscript will +# eventually pick the "correct" entry. I.e. if this userscript is bound to +# "zl", the user has to press "zl" until the correct username shows up in the +# login form. +choose_entry_random() { + local nr=${#files[@]} + file="${files[$((RANDOM % nr))]}" + # Warn user, that there might be other matching password entries + if [ "$nr" -gt 1 ] ; then + msg "Picked $file out of $nr entries: ${files[*]}" + fi +} + +# another implementation would be to ask the user via some menu (like rofi or +# dmenu or zenity or even qutebrowser completion in future?) which entry to +# pick +MENU_COMMAND=( head -n 1 ) +# whether to show the menu if there is only one entry in it +menu_if_one_entry=0 +choose_entry_menu() { + local nr=${#files[@]} + if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then + file="${files[0]}" + else + file=$( printf '%s\n' "${files[@]}" | "${MENU_COMMAND[@]}" ) + fi +} + +choose_entry_rofi() { + MENU_COMMAND=( rofi -p "qutebrowser> " -dmenu + -mesg $'Pick a password entry for '"${QUTE_URL//&/&}"'' ) + choose_entry_menu || true +} + +choose_entry_zenity() { + MENU_COMMAND=( zenity --list --title "qutebrowser password fill" + --text "Pick the password entry:" + --column "Name" ) + choose_entry_menu || true +} + +choose_entry_zenity_radio() { + zenity_helper() { + awk '{ print $0 ; print $0 }' \ + | zenity --list --radiolist \ + --title "qutebrowser password fill" \ + --text "Pick the password entry:" \ + --column " " --column "Name" + } + MENU_COMMAND=( zenity_helper ) + choose_entry_menu || true +} + +# ======================================================= +# backend: PASS + +# configuration options: +match_filename=1 # whether allowing entry match by filepath +match_line=0 # whether allowing entry match by URL-Pattern in file + # Note: match_line=1 gets very slow, even for small password stores! +match_line_pattern='^url: .*' # applied using grep -iE +user_pattern='^(user|username|login): ' + +GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" ) +GPG="gpg" +export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}" +command -v gpg2 &>/dev/null && GPG="gpg2" +[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" ) + +pass_backend() { + init() { + PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}" + if ! [ -d "$PREFIX" ] ; then + die "Can not open password store dir »$PREFIX«" + fi + } + query_entries() { + local url="$1" + + if ((match_line)) ; then + # add entries with matching URL-tag + while read -r -d "" passfile ; do + if $GPG "${GPG_OPTS[@]}" -d "$passfile" \ + | grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null + then + passfile="${passfile#$PREFIX}" + passfile="${passfile#/}" + files+=( "${passfile%.gpg}" ) + fi + done < <(find -L "$PREFIX" -iname '*.gpg' -print0) + fi + if ((match_filename)) ; then + # add entries with matching filepath + while read -r passfile ; do + passfile="${passfile#$PREFIX}" + passfile="${passfile#/}" + files+=( "${passfile%.gpg}" ) + done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url") + fi + } + open_entry() { + local path="$PREFIX/${1}.gpg" + password="" + local firstline=1 + while read -r line ; do + if ((firstline)) ; then + password="$line" + firstline=0 + else + if [[ $line =~ $user_pattern ]] ; then + # remove the matching prefix "user: " from the beginning of the line + username=${line#${BASH_REMATCH[0]}} + break + fi + fi + done < <($GPG "${GPG_OPTS[@]}" -d "$path" | awk 1 ) + } +} +# ======================================================= + +# ======================================================= +# backend: secret +secret_backend() { + init() { + return + } + query_entries() { + local domain="$1" + while read -r line ; do + if [[ "$line" == "attribute.username = "* ]] ; then + files+=("$domain ${line:21}") + fi + done < <( secret-tool search --unlock --all domain "$domain" 2>&1 ) + } + open_entry() { + local domain="${1%% *}" + username="${1#* }" + password=$(secret-tool lookup domain "$domain" username "$username") + } +} +# ======================================================= + +# load some sane default backend +reset_backend +pass_backend +# load configuration +QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/} +PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc} +if [ -f "$PWFILL_CONFIG" ] ; then + # shellcheck source=/dev/null + source "$PWFILL_CONFIG" +fi +init + +simplify_url "$QUTE_URL" +query_entries "${simple_url}" +no_entries_found +# remove duplicates +mapfile -t files < <(printf '%s\n' "${files[@]}" | sort | uniq ) +choose_entry +if [ -z "$file" ] ; then + # choose_entry didn't want any of these entries + exit 0 +fi +open_entry "$file" +#username="$(date)" +#password="XYZ" +#msg "$username, ${#password}" + +[ -n "$username" ] || die "Username not set in entry $file" +[ -n "$password" ] || die "Password not set in entry $file" + +js() { +cat < 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 = "$(javascript_escape "${username}")"; + input.dispatchEvent(new Event('change')); + input.blur(); + } + if (input.type == "password") { + input.focus(); + input.value = "$(javascript_escape "${password}")"; + input.dispatchEvent(new Event('change')); + input.blur(); + } + } + }; + + var forms = document.getElementsByTagName("form"); + for (i = 0; i < forms.length; i++) { + if (hasPasswordField(forms[i])) { + loadData2Form(forms[i]); + } + } +EOF +} + +printjs() { + js | sed 's,//.*$,,' | tr '\n' ' ' +} +echo "jseval -q $(printjs)" >> "$QUTE_FIFO" diff --git a/conf.d/qutebrowser/userscripts/qute-pass b/conf.d/qutebrowser/userscripts/qute-pass index 184bd69..4197816 100755 --- a/conf.d/qutebrowser/userscripts/qute-pass +++ b/conf.d/qutebrowser/userscripts/qute-pass @@ -122,7 +122,7 @@ def find_pass_candidates(domain, password_store_path): pass_path = path[len(password_store_path):] secrets = fnmatch.filter(file_names, '*.gpg') candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets) - + # pass_path = path[len(password_store_path):] # split_path = pass_path.split(os.path.sep) # for secret in secrets: @@ -241,7 +241,7 @@ def main(arguments): fake_key_raw(password) if arguments.insert_mode: - qute_command('enter-mode insert') + qute_command('mode-enter insert') return ExitCodes.SUCCESS