Merge remote-tracking branch 'origin/pr/7196'
This commit is contained in:
commit
870fccb366
|
|
@ -43,6 +43,8 @@ config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234',
|
|||
config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
|
||||
```
|
||||
|
||||
To manage multiple accounts you also need [rofi](https://github.com/davatorium/rofi) installed.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
|
|
@ -65,6 +67,26 @@ Therefore you need to have a public-key-pair readily set up.
|
|||
GPG might then ask for your private-key password whenever you query the database for login credentials.
|
||||
|
||||
|
||||
# TOTP
|
||||
|
||||
This script recently received experimental TOTP support.
|
||||
To use it, you need to have working TOTP authentication within KeepassXC.
|
||||
Then call `qute-keepassxc` with the `--totp` flags.
|
||||
|
||||
For example, I have the following line in my `config.py`:
|
||||
|
||||
```python
|
||||
config.bind('pt', 'spawn --userscript qute-keepassxc --key ABC1234 --totp', mode='normal')
|
||||
```
|
||||
|
||||
For now this script will simply insert the TOTP-token into the currently selected
|
||||
input field, since I have not yet found a reliable way to identify the correct field
|
||||
within all existing login forms.
|
||||
Thus you need to manually select the TOTP input field, press escape to leave input
|
||||
mode and then enter `pt` to fill in the token (or configure another key-binding for
|
||||
insert mode if you prefer that).
|
||||
|
||||
|
||||
[1]: https://keepassxc.org/
|
||||
[2]: https://qutebrowser.org/
|
||||
[3]: https://gnupg.org/
|
||||
|
|
@ -88,6 +110,8 @@ import nacl.public
|
|||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
|
||||
parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
|
||||
parser.add_argument('--totp', action='store_true',
|
||||
help="Fill in current TOTP field instead of username/password")
|
||||
parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
|
||||
help='Path to KeepassXC browser socket')
|
||||
parser.add_argument('--key', '-k', default='alice@example.com',
|
||||
|
|
@ -160,7 +184,7 @@ class KeepassXC:
|
|||
action = 'test-associate',
|
||||
id = self.id,
|
||||
key = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
|
||||
))
|
||||
), triggerUnlock = 'true')
|
||||
return self.recv_msg()['success'] == 'true'
|
||||
|
||||
def associate(self):
|
||||
|
|
@ -180,6 +204,16 @@ class KeepassXC:
|
|||
))
|
||||
return self.recv_msg()['entries']
|
||||
|
||||
def get_totp(self, uuid):
|
||||
self.send_msg(dict(
|
||||
action = 'get-totp',
|
||||
uuid = uuid
|
||||
))
|
||||
response = self.recv_msg()
|
||||
if response['success'] != 'true' or not response['totp']:
|
||||
return None
|
||||
return response['totp']
|
||||
|
||||
def send_raw_msg(self, msg):
|
||||
self.sock.send( json.dumps(msg).encode('utf-8') )
|
||||
|
||||
|
|
@ -274,6 +308,30 @@ def connect_to_keepassxc(args):
|
|||
return kp
|
||||
|
||||
|
||||
def select_account(creds):
|
||||
try:
|
||||
if len(creds) == 1:
|
||||
return creds[0]
|
||||
idx = subprocess.check_output(
|
||||
['rofi', '-dmenu', '-format', 'i', '-matching', 'fuzzy',
|
||||
'-p', 'Search',
|
||||
'-mesg', '<b>qute-keepassxc</b>: select an account, please!'],
|
||||
input=b"\n".join(c['login'].encode('utf-8') for c in creds)
|
||||
)
|
||||
idx = int(idx)
|
||||
if idx < 0:
|
||||
return None
|
||||
return creds[idx]
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
error("rofi not found. Please install rofi to select from multiple credentials")
|
||||
return creds[0]
|
||||
except Exception as e:
|
||||
error(f"Error while picking account: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def make_js_code(username, password):
|
||||
return ' '.join("""
|
||||
function isVisible(elem) {
|
||||
|
|
@ -335,6 +393,21 @@ def make_js_code(username, password):
|
|||
""".splitlines()) % (json.dumps(username), json.dumps(password))
|
||||
|
||||
|
||||
def make_js_totp_code(totp):
|
||||
return ' '.join("""
|
||||
(function () {
|
||||
var input = document.activeElement;
|
||||
if (!input || input.tagName !== "INPUT") {
|
||||
alert("No TOTP input field selected");
|
||||
return;
|
||||
}
|
||||
input.value = %s;
|
||||
input.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
input.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
})();
|
||||
""".splitlines()) % (json.dumps(totp),)
|
||||
|
||||
|
||||
def main():
|
||||
if 'QUTE_FIFO' not in os.environ:
|
||||
print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
|
||||
|
|
@ -351,10 +424,21 @@ def main():
|
|||
if not creds:
|
||||
error('No credentials found')
|
||||
return
|
||||
# TODO: handle multiple matches
|
||||
name, pw = creds[0]['login'], creds[0]['password']
|
||||
if name and pw:
|
||||
qute('jseval -q ' + make_js_code(name, pw))
|
||||
cred = select_account(creds)
|
||||
if not cred:
|
||||
error('No credentials selected')
|
||||
return
|
||||
if args.totp:
|
||||
uuid = cred['uuid']
|
||||
totp = kp.get_totp(uuid)
|
||||
if not totp:
|
||||
error('No TOTP key found')
|
||||
return
|
||||
qute('jseval -q ' + make_js_totp_code(totp))
|
||||
else:
|
||||
name, pw = cred['login'], cred['password']
|
||||
if name and pw:
|
||||
qute('jseval -q ' + make_js_code(name, pw))
|
||||
except Exception as e:
|
||||
error(str(e))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue