#!/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"