diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill new file mode 100755 index 000000000..bc12e228b --- /dev/null +++ b/misc/userscripts/password_fill @@ -0,0 +1,342 @@ +#!/bin/bash -e +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 freenode. + + $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). + +Behaviour: + 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 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 a escaped way, such that it can savely + # be used within javascripts double quotes + 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 protocoll 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 behaviour 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 beahviour is to drop another level of subdomains until search hits +# are found: +no_entries_found() { + while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do + 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 entrie 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)}" +which 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 wth 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" ) + } +} +# ======================================================= + +# 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 + 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 <> "$QUTE_FIFO"