This file is indexed.

/usr/share/qutebrowser/userscripts/qute-pass is in qutebrowser 1.1.1-1.

This file is owned by root:root, with mode 0o755.

The actual contents of the file can be viewed below.

  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
#! /usr/bin/python3

# Copyright 2017 Chris Braun (cryzed) <cryzed@googlemail.com>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.

"""
Insert login information using pass and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...). A short
demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif.
"""

USAGE = """The domain of the site has to appear as a segment in the pass path, for example: "github.com/cryzed" or
"websites/github.com". How the username and password are determined is freely configurable using the CLI arguments. The
login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms."""

EPILOG = """Dependencies: tldextract (Python 3 module), pass.
For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts.

WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
you decide to submit a crash report!"""

import argparse
import enum
import fnmatch
import functools
import os
import re
import shlex
import subprocess
import sys

import tldextract

argument_parser = argparse.ArgumentParser(description=__doc__, usage=USAGE, epilog=EPILOG)
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--password-store', '-p', default=os.path.expanduser('~/.password-store'),
                             help='Path to your pass password-store')
argument_parser.add_argument('--username-pattern', '-u', default=r'.*/(.+)',
                             help='Regular expression that matches the username')
argument_parser.add_argument('--username-target', '-U', choices=['path', 'secret'], default='path',
                             help='The target for the username regular expression')
argument_parser.add_argument('--password-pattern', '-P', default=r'(.*)',
                             help='Regular expression that matches the password')
argument_parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu',
                             help='Invocation used to execute a dmenu-provider')
argument_parser.add_argument('--no-insert-mode', '-n', dest='insert_mode', action='store_false',
                             help="Don't automatically enter insert mode")
argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
                             help='Encoding used to communicate with subprocesses')
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
                             help='Merge pass candidates for fully-qualified and registered domain name')
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')

stderr = functools.partial(print, file=sys.stderr)


class ExitCodes(enum.IntEnum):
    SUCCESS = 0
    FAILURE = 1
    # 1 is automatically used if Python throws an exception
    NO_PASS_CANDIDATES = 2
    COULD_NOT_MATCH_USERNAME = 3
    COULD_NOT_MATCH_PASSWORD = 4


def qute_command(command):
    with open(os.environ['QUTE_FIFO'], 'w') as fifo:
        fifo.write(command + '\n')
        fifo.flush()


def find_pass_candidates(domain, password_store_path):
    candidates = []
    for path, directories, file_names in os.walk(password_store_path):
        if directories or domain not in path.split(os.path.sep):
            continue

        # Strip password store path prefix to get the relative pass path
        pass_path = path[len(password_store_path) + 1:]
        secrets = fnmatch.filter(file_names, '*.gpg')
        candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets)
    return candidates


def pass_(path, encoding):
    process = subprocess.run(['pass', path], stdout=subprocess.PIPE)
    return process.stdout.decode(encoding).strip()


def dmenu(items, invocation, encoding):
    command = shlex.split(invocation)
    process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
    return process.stdout.decode(encoding).strip()


def main(arguments):
    if not arguments.url:
        argument_parser.print_help()
        return ExitCodes.FAILURE

    extract_result = tldextract.extract(arguments.url)

    # Expand potential ~ in paths, since this script won't be called from a shell that does it for us
    password_store_path = os.path.expanduser(arguments.password_store)

    # Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
    # the registered domain name and finally: the IPv4 address if that's what the URL represents
    candidates = set()
    for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.ipv4]):
        target_candidates = find_pass_candidates(target, password_store_path)
        if not target_candidates:
            continue

        candidates.update(target_candidates)
        if not arguments.merge_candidates:
            break
    else:
        if not candidates:
            stderr('No pass candidates for URL {!r} found!'.format(arguments.url))
            return ExitCodes.NO_PASS_CANDIDATES

    selection = candidates.pop() if len(candidates) == 1 else dmenu(sorted(candidates), arguments.dmenu_invocation,
                                                                    arguments.io_encoding)
    # Nothing was selected, simply return
    if not selection:
        return ExitCodes.SUCCESS

    secret = pass_(selection, arguments.io_encoding)

    # Match username
    target = selection if arguments.username_target == 'path' else secret
    match = re.match(arguments.username_pattern, target)
    if not match:
        stderr('Failed to match username pattern on {}!'.format(arguments.username_target))
        return ExitCodes.COULD_NOT_MATCH_USERNAME
    username = match.group(1)

    # Match password
    match = re.match(arguments.password_pattern, secret)
    if not match:
        stderr('Failed to match password pattern on secret!')
        return ExitCodes.COULD_NOT_MATCH_PASSWORD
    password = match.group(1)

    insert_mode = ';; enter-mode insert' if arguments.insert_mode else ''
    if arguments.username_only:
        qute_command('fake-key {}{}'.format(username, insert_mode))
    elif arguments.password_only:
        qute_command('fake-key {}{}'.format(password, insert_mode))
    else:
        # Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
        # back into insert-mode, so the form can be directly submitted by hitting enter afterwards
        qute_command('fake-key {} ;; fake-key <Tab> ;; fake-key {}{}'.format(username, password, insert_mode))

    return ExitCodes.SUCCESS


if __name__ == '__main__':
    arguments = argument_parser.parse_args()
    sys.exit(main(arguments))