From d73491b0c80254bea2f327e7acd3ff616f21f037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Fri, 14 Aug 2015 23:53:52 +0200 Subject: [PATCH 1/7] Add password_fill userscript Add a configurable userscript that fills login forms (i.e. the fiels "Username" and "Password) of websites using a configurable backend where the actual passwords are stored. The only backend yet is using the password store "pass". --- misc/userscripts/password_fill | 276 +++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100755 misc/userscripts/password_fill diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill new file mode 100755 index 000000000..15114044d --- /dev/null +++ b/misc/userscripts/password_fill @@ -0,0 +1,276 @@ +#!/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 + +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. + +EOF +} + +set -o pipefail +shopt -s nocasematch + +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 +} + +# ======================================================= # +# 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##www.}" # remove www. subdomain +} + + +# 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_random +} + +# 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 ) +choose_entry_menu() { + local nr=${#files[@]} + if [ "$nr" -eq 1 ] ; 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=1 # whether allowing entry match by URL-Pattern in file +match_line_pattern='^url: .*' # applied using grep -iE +user_pattern='^(user|username): ' + +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=${PFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc} +if [ -f "$CONFIG_FILE" ] ; then + source "$CONFIG_FILE" +fi +init + +simplify_url "$QUTE_URL" +query_entries "${simple_url}" +if [ 0 -eq "${#files[@]}" ] ; then + die "No entry found for »$simple_url«" +fi +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" + +[ -n "$username" ] || die "Username not set in entry $file" +[ -n "$password" ] || die "Password not set in entry $file" + +js() { +cat <> "$QUTE_FIFO" From fb5e6e6c35eaf5d3112bc20d73bc53a4c9acf91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Sat, 5 Dec 2015 12:11:10 +0100 Subject: [PATCH 2/7] More sane defaults - Remove Port from URL - Use zenity per default - Allow customization of handling of no entries are found --- misc/userscripts/password_fill | 50 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 15114044d..7ce989902 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -62,9 +62,41 @@ 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 + simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") + query_entries "$simple_url" + #die "No entry found for »$simple_url«" + 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 @@ -87,7 +119,7 @@ reset_backend() { # choose_entry() is expected to choose one entry from the array $files and # write it to the variable $file. choose_entry() { - choose_entry_random + choose_entry_zenity } # The default implementation chooses a random entry from the array. So if there @@ -147,7 +179,8 @@ choose_entry_zenity_radio() { # configuration options: match_filename=1 # whether allowing entry match by filepath -match_line=1 # whether allowing entry match by URL-Pattern in file +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): ' @@ -213,17 +246,17 @@ reset_backend pass_backend # load configuration QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/} -PWFILL_CONFIG=${PFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc} -if [ -f "$CONFIG_FILE" ] ; then - source "$CONFIG_FILE" +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}" -if [ 0 -eq "${#files[@]}" ] ; then - die "No entry found for »$simple_url«" -fi +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 @@ -232,6 +265,7 @@ 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" From ffdc0f664f77c25bd82b935d8c439bd0ceed6d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Sat, 5 Dec 2015 12:26:14 +0100 Subject: [PATCH 3/7] Ask for confirmation on fuzzy entry matching --- misc/userscripts/password_fill | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 7ce989902..2a5c21792 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -19,7 +19,10 @@ 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. + 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) EOF } @@ -92,6 +95,8 @@ no_entries_found() { simple_url=$(sed 's,^[^.]*\.,,' <<< "$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«" @@ -140,9 +145,11 @@ choose_entry_random() { # 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 ] ; then + if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then file="${files[0]}" else file=$( printf "%s\n" "${files[@]}" | "${MENU_COMMAND[@]}" ) From 497a6e072043698d040681b4fcf5166a07535fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Wed, 9 Dec 2015 23:25:55 +0100 Subject: [PATCH 4/7] Fixup url simplifier loop Now, the loop which successively simplifies the url, always terminates. --- misc/userscripts/password_fill | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 2a5c21792..4da4ebe90 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -92,7 +92,14 @@ simplify_url() { # are found: no_entries_found() { while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do - simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") + 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 From e4b809927fa6a0f11f28fc2d0262e8c1ddb13388 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Fri, 11 Dec 2015 16:23:47 +0100 Subject: [PATCH 5/7] Escape password properly in generated JS. --- misc/userscripts/password_fill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 4da4ebe90..2304502db 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -304,7 +304,7 @@ cat < Date: Fri, 11 Dec 2015 16:54:16 +0100 Subject: [PATCH 6/7] Add documentation in password_fill Describe usage and configuration. Also allow "login:" as a prefix for login name in pass entries. --- misc/userscripts/password_fill | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 2304502db..7d0ad3b9b 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -15,6 +15,13 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode. 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 @@ -24,11 +31,16 @@ Behaviour: 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 +shopt -s nocasematch # make regexp matching in bash case insensitive if [ -z "$QUTE_FIFO" ] ; then help @@ -196,7 +208,7 @@ 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): ' +user_pattern='^(user|username|login): ' GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" ) GPG="gpg" From 26f2ae5ad014b1e41031bdb50d62d9dd9c6a718f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Fri, 11 Dec 2015 17:04:50 +0100 Subject: [PATCH 7/7] Do proper javascript escaping in password_fill --- misc/userscripts/password_fill | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 7d0ad3b9b..bc12e228b 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -60,6 +60,12 @@ die() { 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 # ======================================================= # @@ -313,10 +319,10 @@ cat <