summaryrefslogtreecommitdiff
path: root/misc/userscripts/password_fill
blob: c46253d4192bd788dc624d0a32ae664d77a9f470 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#!/usr/bin/env bash
help() {
    blink=$'\e[1;31m' reset=$'\e[0m'
cat <<EOF
This script can only be used as a userscript for qutebrowser
2015, Thorsten Wißmann <edu _at_ thorsten-wissmann _dot_ de>
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 <b>'"${QUTE_URL//&/&amp;}"'</b>' )
    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 <<EOF
    function isVisible(elem) {
        var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);

        if (style.getPropertyValue("visibility") !== "visible" ||
            style.getPropertyValue("display") === "none" ||
            style.getPropertyValue("opacity") === "0") {
            return false;
        }

        return elem.offsetWidth > 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"