| password_fill - dotfiles - These are my dotfiles. There are many like it, but t… | |
| git clone git://jay.scot/dotfiles | |
| Log | |
| Files | |
| Refs | |
| README | |
| --- | |
| password_fill (13378B) | |
| --- | |
| 1 #!/usr/bin/env bash | |
| 2 help() { | |
| 3 blink=$'\e[1;31m' reset=$'\e[0m' | |
| 4 cat <<EOF | |
| 5 This script can only be used as a userscript for qutebrowser | |
| 6 2015, Thorsten Wißmann <edu _at_ thorsten-wissmann _dot_ de> | |
| 7 In case of questions or suggestions, do not hesitate to send me an E-Mai… | |
| 8 directly ask me via IRC (nickname thorsten\`) in #qutebrowser on Libera … | |
| 9 | |
| 10 $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset | |
| 11 WARNING: the passwords are stored in qutebrowser's | |
| 12 debug log reachable via the url qute://log | |
| 13 $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset | |
| 14 | |
| 15 Usage: run as a userscript form qutebrowser, e.g.: | |
| 16 spawn --userscript ~/.config/qutebrowser/password_fill | |
| 17 | |
| 18 Pass backend: (see also passwordstore.org) | |
| 19 This script expects pass to store the credentials of each page in an e… | |
| 20 file, where the filename (or filepath) contains the domain of the resp… | |
| 21 page. The first line of the file must contain the password, the login … | |
| 22 must be contained in a later line beginning with "user:", "login:", or | |
| 23 "username:" (configurable by the user_pattern variable). | |
| 24 | |
| 25 Behavior: | |
| 26 It will try to find a username/password entry in the configured backend | |
| 27 (currently only pass) for the current website and will load that pair … | |
| 28 username and password to any form on the current page that has some pa… | |
| 29 entry field. If multiple entries are found, a zenity menu is offered. | |
| 30 | |
| 31 If no entry is found, then it crops subdomains from the url if at leas… | |
| 32 entry is found in the backend. (In that case, it always shows a menu) | |
| 33 | |
| 34 Configuration: | |
| 35 This script loads the bash script ~/.config/qutebrowser/password_fill_… | |
| 36 it exists), so you can change any configuration variable and overwrite… | |
| 37 function you like. | |
| 38 | |
| 39 EOF | |
| 40 } | |
| 41 | |
| 42 set -o errexit | |
| 43 set -o pipefail | |
| 44 shopt -s nocasematch # make regexp matching in bash case insensitive | |
| 45 | |
| 46 if [ -z "$QUTE_FIFO" ] ; then | |
| 47 help | |
| 48 exit | |
| 49 fi | |
| 50 | |
| 51 error() { | |
| 52 local msg="$*" | |
| 53 echo "message-error '${msg//\'/\\\'}'" >> "$QUTE_FIFO" | |
| 54 } | |
| 55 msg() { | |
| 56 local msg="$*" | |
| 57 echo "message-info '${msg//\'/\\\'}'" >> "$QUTE_FIFO" | |
| 58 } | |
| 59 die() { | |
| 60 error "$*" | |
| 61 exit 0 | |
| 62 } | |
| 63 | |
| 64 javascript_escape() { | |
| 65 # print the first argument in an escaped way, such that it can safely | |
| 66 # be used within javascripts double quotes | |
| 67 # shellcheck disable=SC2001 | |
| 68 sed "s,[\\\\'\"],\\\\&,g" <<< "$1" | |
| 69 } | |
| 70 | |
| 71 # ======================================================= # | |
| 72 # CONFIGURATION | |
| 73 # ======================================================= # | |
| 74 # The configuration file is per default located in | |
| 75 # ~/.config/qutebrowser/password_fill_rc and is a bash script that is lo… | |
| 76 # later in the present script. So basically you can replace all of the | |
| 77 # following definitions and make them fit your needs. | |
| 78 | |
| 79 # The following simplifies a URL to the domain (e.g. "wiki.qutebrowser.o… | |
| 80 # which is later used to search the correct entries in the password back… | |
| 81 # you e.g. don't want the "www." to be removed or if you want to disting… | |
| 82 # between different paths on the same domain. | |
| 83 | |
| 84 simplify_url() { | |
| 85 simple_url="${1##*://}" # remove protocol specification | |
| 86 simple_url="${simple_url%%\?*}" # remove GET parameters | |
| 87 simple_url="${simple_url%%/*}" # remove directory path | |
| 88 simple_url="${simple_url%:*}" # remove port | |
| 89 simple_url="${simple_url##www.}" # remove www. subdomain | |
| 90 } | |
| 91 | |
| 92 # no_entries_found() is called if the first query_entries() call did not… | |
| 93 # any matching entries. Multiple implementations are possible: | |
| 94 # The easiest behavior is to quit: | |
| 95 #no_entries_found() { | |
| 96 # if [ 0 -eq "${#files[@]}" ] ; then | |
| 97 # die "No entry found for »$simple_url«" | |
| 98 # fi | |
| 99 #} | |
| 100 # But you could also fill the files array with all entries from your pas… | |
| 101 # if the first db query did not find anything | |
| 102 # no_entries_found() { | |
| 103 # if [ 0 -eq "${#files[@]}" ] ; then | |
| 104 # query_entries "" | |
| 105 # if [ 0 -eq "${#files[@]}" ] ; then | |
| 106 # die "No entry found for »$simple_url«" | |
| 107 # fi | |
| 108 # fi | |
| 109 # } | |
| 110 | |
| 111 # Another behavior is to drop another level of subdomains until search h… | |
| 112 # are found: | |
| 113 no_entries_found() { | |
| 114 while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do | |
| 115 # shellcheck disable=SC2001 | |
| 116 shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") | |
| 117 if [ "$shorter_simple_url" = "$simple_url" ] ; then | |
| 118 # if no dot, then even remove the top level domain | |
| 119 simple_url="" | |
| 120 query_entries "$simple_url" | |
| 121 break | |
| 122 fi | |
| 123 simple_url="$shorter_simple_url" | |
| 124 query_entries "$simple_url" | |
| 125 #die "No entry found for »$simple_url«" | |
| 126 # enforce menu if we do "fuzzy" matching | |
| 127 menu_if_one_entry=1 | |
| 128 done | |
| 129 if [ 0 -eq "${#files[@]}" ] ; then | |
| 130 die "No entry found for »$simple_url«" | |
| 131 fi | |
| 132 } | |
| 133 | |
| 134 # Backend implementations tell, how the actual password store is accesse… | |
| 135 # Right now, there is only one fully functional password backend, namely… | |
| 136 # the program "pass". | |
| 137 # A password backend consists of three actions: | |
| 138 # - init() initializes backend-specific things and does sanity checks. | |
| 139 # - query_entries() is called with a simplified url and is expected to … | |
| 140 # the bash array $files with the names of matching password entries. … | |
| 141 # are no requirements how these names should look like. | |
| 142 # - open_entry() is called with some specific entry of the $files array… | |
| 143 # expected to write the username of that entry to the $username varia… | |
| 144 # the corresponding password to $password | |
| 145 | |
| 146 # shellcheck disable=SC2329 | |
| 147 reset_backend() { | |
| 148 init() { true ; } | |
| 149 query_entries() { true ; } | |
| 150 open_entry() { true ; } | |
| 151 } | |
| 152 | |
| 153 # choose_entry() is expected to choose one entry from the array $files a… | |
| 154 # write it to the variable $file. | |
| 155 choose_entry() { | |
| 156 choose_entry_zenity | |
| 157 } | |
| 158 | |
| 159 # The default implementation chooses a random entry from the array. So i… | |
| 160 # are multiple matching entries, multiple calls to this userscript will | |
| 161 # eventually pick the "correct" entry. I.e. if this userscript is bound … | |
| 162 # "zl", the user has to press "zl" until the correct username shows up i… | |
| 163 # login form. | |
| 164 choose_entry_random() { | |
| 165 local nr=${#files[@]} | |
| 166 file="${files[$((RANDOM % nr))]}" | |
| 167 # Warn user, that there might be other matching password entries | |
| 168 if [ "$nr" -gt 1 ] ; then | |
| 169 msg "Picked $file out of $nr entries: ${files[*]}" | |
| 170 fi | |
| 171 } | |
| 172 | |
| 173 # another implementation would be to ask the user via some menu (like ro… | |
| 174 # dmenu or zenity or even qutebrowser completion in future?) which entry… | |
| 175 # pick | |
| 176 MENU_COMMAND=( head -n 1 ) | |
| 177 # whether to show the menu if there is only one entry in it | |
| 178 menu_if_one_entry=0 | |
| 179 choose_entry_menu() { | |
| 180 local nr=${#files[@]} | |
| 181 if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then | |
| 182 file="${files[0]}" | |
| 183 else | |
| 184 file=$( printf '%s\n' "${files[@]}" | "${MENU_COMMAND[@]}" ) | |
| 185 fi | |
| 186 } | |
| 187 | |
| 188 choose_entry_rofi() { | |
| 189 MENU_COMMAND=( rofi -p "qutebrowser> " -dmenu | |
| 190 -mesg $'Pick a password entry for <b>'"${QUTE_UR… | |
| 191 choose_entry_menu || true | |
| 192 } | |
| 193 | |
| 194 choose_entry_zenity() { | |
| 195 MENU_COMMAND=( zenity --list --title "qutebrowser password fill" | |
| 196 --text "Pick the password entry:" | |
| 197 --column "Name" ) | |
| 198 choose_entry_menu || true | |
| 199 } | |
| 200 | |
| 201 choose_entry_zenity_radio() { | |
| 202 # shellcheck disable=SC2329 | |
| 203 zenity_helper() { | |
| 204 awk '{ print $0 ; print $0 }' \ | |
| 205 | zenity --list --radiolist \ | |
| 206 --title "qutebrowser password fill" \ | |
| 207 --text "Pick the password entry:" \ | |
| 208 --column " " --column "Name" | |
| 209 } | |
| 210 MENU_COMMAND=( zenity_helper ) | |
| 211 choose_entry_menu || true | |
| 212 } | |
| 213 | |
| 214 # ======================================================= | |
| 215 # backend: PASS | |
| 216 | |
| 217 # configuration options: | |
| 218 match_filename=1 # whether allowing entry match by filepath | |
| 219 match_line=0 # whether allowing entry match by URL-Pattern in file | |
| 220 # Note: match_line=1 gets very slow, even for small pas… | |
| 221 match_line_pattern='^url: .*' # applied using grep -iE | |
| 222 user_pattern='^(user|username|login): ' | |
| 223 | |
| 224 GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" ) | |
| 225 GPG="gpg" | |
| 226 export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}" | |
| 227 command -v gpg2 &>/dev/null && GPG="gpg2" | |
| 228 [[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--u… | |
| 229 | |
| 230 pass_backend() { | |
| 231 init() { | |
| 232 PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}" | |
| 233 if ! [ -d "$PREFIX" ] ; then | |
| 234 die "Can not open password store dir »$PREFIX«" | |
| 235 fi | |
| 236 } | |
| 237 query_entries() { | |
| 238 local url="$1" | |
| 239 | |
| 240 if ((match_line)) ; then | |
| 241 # add entries with matching URL-tag | |
| 242 while read -r -d "" passfile ; do | |
| 243 if $GPG "${GPG_OPTS[@]}" -d "$passfile" \ | |
| 244 | grep --max-count=1 -iE "${match_line_pattern}${ur… | |
| 245 then | |
| 246 passfile="${passfile#"$PREFIX"}" | |
| 247 passfile="${passfile#/}" | |
| 248 files+=( "${passfile%.gpg}" ) | |
| 249 fi | |
| 250 done < <(find -L "$PREFIX" -iname '*.gpg' -print0) | |
| 251 fi | |
| 252 if ((match_filename)) ; then | |
| 253 # add entries with matching filepath | |
| 254 while read -r passfile ; do | |
| 255 passfile="${passfile#"$PREFIX"}" | |
| 256 passfile="${passfile#/}" | |
| 257 files+=( "${passfile%.gpg}" ) | |
| 258 done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url") | |
| 259 fi | |
| 260 } | |
| 261 open_entry() { | |
| 262 local path="$PREFIX/${1}.gpg" | |
| 263 password="" | |
| 264 local firstline=1 | |
| 265 while read -r line ; do | |
| 266 if ((firstline)) ; then | |
| 267 password="$line" | |
| 268 firstline=0 | |
| 269 else | |
| 270 if [[ $line =~ $user_pattern ]] ; then | |
| 271 # remove the matching prefix "user: " from the begin… | |
| 272 username=${line#"${BASH_REMATCH[0]}"} | |
| 273 break | |
| 274 fi | |
| 275 fi | |
| 276 done < <($GPG "${GPG_OPTS[@]}" -d "$path" | awk 1 ) | |
| 277 } | |
| 278 } | |
| 279 # ======================================================= | |
| 280 | |
| 281 # ======================================================= | |
| 282 # backend: secret | |
| 283 # shellcheck disable=SC2329 | |
| 284 secret_backend() { | |
| 285 init() { | |
| 286 return | |
| 287 } | |
| 288 query_entries() { | |
| 289 local domain="$1" | |
| 290 while read -r line ; do | |
| 291 if [[ "$line" == "attribute.username = "* ]] ; then | |
| 292 files+=("$domain ${line:21}") | |
| 293 fi | |
| 294 done < <( secret-tool search --unlock --all domain "$domain" 2>&… | |
| 295 } | |
| 296 open_entry() { | |
| 297 local domain="${1%% *}" | |
| 298 username="${1#* }" | |
| 299 password=$(secret-tool lookup domain "$domain" username "$userna… | |
| 300 } | |
| 301 } | |
| 302 # ======================================================= | |
| 303 | |
| 304 # load some sane default backend | |
| 305 reset_backend | |
| 306 pass_backend | |
| 307 # load configuration | |
| 308 QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qut… | |
| 309 PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc} | |
| 310 if [ -f "$PWFILL_CONFIG" ] ; then | |
| 311 # shellcheck source=/dev/null | |
| 312 source "$PWFILL_CONFIG" | |
| 313 fi | |
| 314 init | |
| 315 | |
| 316 simplify_url "$QUTE_URL" | |
| 317 query_entries "${simple_url}" | |
| 318 no_entries_found | |
| 319 # remove duplicates | |
| 320 mapfile -t files < <(printf '%s\n' "${files[@]}" | sort | uniq ) | |
| 321 choose_entry | |
| 322 if [ -z "$file" ] ; then | |
| 323 # choose_entry didn't want any of these entries | |
| 324 exit 0 | |
| 325 fi | |
| 326 open_entry "$file" | |
| 327 #username="$(date)" | |
| 328 #password="XYZ" | |
| 329 #msg "$username, ${#password}" | |
| 330 | |
| 331 [ -n "$username" ] || die "Username not set in entry $file" | |
| 332 [ -n "$password" ] || die "Password not set in entry $file" | |
| 333 | |
| 334 js() { | |
| 335 cat <<EOF | |
| 336 function isVisible(elem) { | |
| 337 var style = elem.ownerDocument.defaultView.getComputedStyle(elem… | |
| 338 | |
| 339 if (style.getPropertyValue("visibility") !== "visible" || | |
| 340 style.getPropertyValue("display") === "none" || | |
| 341 style.getPropertyValue("opacity") === "0") { | |
| 342 return false; | |
| 343 } | |
| 344 | |
| 345 return elem.offsetWidth > 0 && elem.offsetHeight > 0; | |
| 346 }; | |
| 347 function hasPasswordField(form) { | |
| 348 var inputs = form.getElementsByTagName("input"); | |
| 349 for (var j = 0; j < inputs.length; j++) { | |
| 350 var input = inputs[j]; | |
| 351 if (input.type == "password") { | |
| 352 return true; | |
| 353 } | |
| 354 } | |
| 355 return false; | |
| 356 }; | |
| 357 function loadData2Form (form) { | |
| 358 var inputs = form.getElementsByTagName("input"); | |
| 359 for (var j = 0; j < inputs.length; j++) { | |
| 360 var input = inputs[j]; | |
| 361 if (isVisible(input) && (input.type == "text" || input.type … | |
| 362 input.focus(); | |
| 363 input.value = "$(javascript_escape "${username}")"; | |
| 364 input.dispatchEvent(new Event('change')); | |
| 365 input.blur(); | |
| 366 } | |
| 367 if (input.type == "password") { | |
| 368 input.focus(); | |
| 369 input.value = "$(javascript_escape "${password}")"; | |
| 370 input.dispatchEvent(new Event('change')); | |
| 371 input.blur(); | |
| 372 } | |
| 373 } | |
| 374 }; | |
| 375 | |
| 376 var forms = document.getElementsByTagName("form"); | |
| 377 for (i = 0; i < forms.length; i++) { | |
| 378 if (hasPasswordField(forms[i])) { | |
| 379 loadData2Form(forms[i]); | |
| 380 } | |
| 381 } | |
| 382 EOF | |
| 383 } | |
| 384 | |
| 385 printjs() { | |
| 386 js | sed 's,//.*$,,' | tr '\n' ' ' | |
| 387 } | |
| 388 echo "jseval -q $(printjs)" >> "$QUTE_FIFO" |