Add qutebrowser userscripts and minor fixes
This commit is contained in:
		
							parent
							
								
									44e50eabb1
								
							
						
					
					
						commit
						513e3b8263
					
				
					 5 changed files with 427 additions and 6 deletions
				
			
		|  | @ -28,6 +28,7 @@ alias du='du -h' | ||||||
| 
 | 
 | ||||||
| alias free='free -h' | alias free='free -h' | ||||||
| 
 | 
 | ||||||
|  | alias git-apply-clip='clip -out | base64 -d | git apply -' | ||||||
| alias grep='grep --color=auto' | alias grep='grep --color=auto' | ||||||
| alias grep-highlight='grep -e "^" -e' | alias grep-highlight='grep -e "^" -e' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,7 +6,10 @@ def init(c): | ||||||
|     c.colors.webpage.preferred_color_scheme = 'dark' |     c.colors.webpage.preferred_color_scheme = 'dark' | ||||||
| 
 | 
 | ||||||
|     #c.content.proxy = "socks://localhost:9050/" |     #c.content.proxy = "socks://localhost:9050/" | ||||||
|     #c.content.headers.accept_language = "en-IE,en;q=0.9" |     c.content.headers.accept_language = "en-US,en;q=0.5" | ||||||
|  |     c.content.headers.custom = { | ||||||
|  |         "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     c.downloads.position = 'bottom' |     c.downloads.position = 'bottom' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,5 +7,6 @@ XDG_MUSIC_DIR="$HOME/music" | ||||||
| XDG_PICTURES_DIR="$HOME/pictures" | XDG_PICTURES_DIR="$HOME/pictures" | ||||||
| XDG_VIDEOS_DIR="$HOME/videos" | XDG_VIDEOS_DIR="$HOME/videos" | ||||||
| XDG_CONFIG_HOME="$HOME/.config" | XDG_CONFIG_HOME="$HOME/.config" | ||||||
|  | XDG_CONFIG_DIR="$HOME/.config" | ||||||
| XDG_CACHE_DIR="$HOME/.cache" | XDG_CACHE_DIR="$HOME/.cache" | ||||||
| XDG_DATA_HOME="$HOME/.local/share" | XDG_DATA_HOME="$HOME/.local/share" | ||||||
|  |  | ||||||
|  | @ -3,8 +3,6 @@ | ||||||
| import os | import os | ||||||
| import argparse | import argparse | ||||||
| 
 | 
 | ||||||
| import unalix |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def qute(cmd): | def qute(cmd): | ||||||
|     with open(os.environ['QUTE_FIFO'], 'w') as fifo: |     with open(os.environ['QUTE_FIFO'], 'w') as fifo: | ||||||
|  | @ -32,13 +30,17 @@ if __name__ == '__main__': | ||||||
|         args = parse_args() |         args = parse_args() | ||||||
| 
 | 
 | ||||||
|         url = os.environ['QUTE_URL'] |         url = os.environ['QUTE_URL'] | ||||||
|         clean_url = unalix.clear_url(url) |  | ||||||
| 
 | 
 | ||||||
|  |         try: | ||||||
|  |             import unalix | ||||||
|  |             url = unalix.clear_url(url) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
| 
 | 
 | ||||||
|         if args.selection: |         if args.selection: | ||||||
|             qute('yank inline "{}" -s'.format(clean_url)) |             qute('yank inline "{}" -s'.format(url)) | ||||||
|         else: |         else: | ||||||
|             qute('yank inline "{}"'.format(clean_url)) |             qute('yank inline "{}"'.format(url)) | ||||||
| 
 | 
 | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         error(str(e)) |         error(str(e)) | ||||||
|  |  | ||||||
							
								
								
									
										414
									
								
								.local/share/qutebrowser/userscripts/qute-keepassxc
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										414
									
								
								.local/share/qutebrowser/userscripts/qute-keepassxc
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,414 @@ | ||||||
|  | #!/usr/bin/env python3 | ||||||
|  | 
 | ||||||
|  | # Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.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 <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | # Introduction | ||||||
|  | 
 | ||||||
|  | This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Installation | ||||||
|  | 
 | ||||||
|  | First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3]. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Third, install the python module `pynacl`. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Finally, adapt your qutebrowser config. | ||||||
|  | You can e.g. add the following lines to your `~/.config/qutebrowser/config.py` | ||||||
|  | Remember to replace `ABC1234` with your actual GPG key. | ||||||
|  | 
 | ||||||
|  | ```python | ||||||
|  | config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert') | ||||||
|  | config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal') | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Usage | ||||||
|  | 
 | ||||||
|  | If you are on a webpage with a login form, simply activate one of the configured key-bindings. | ||||||
|  | 
 | ||||||
|  | The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension. | ||||||
|  | Just provide a name of your choice and accept the request if nothing looks fishy. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # How it works | ||||||
|  | 
 | ||||||
|  | This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4]. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | This script needs to store the key used to associate with your KeepassXC instance somewhere. | ||||||
|  | Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way | ||||||
|  | by storing the key in encrypted form using GPG. | ||||||
|  | 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. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [1]: https://keepassxc.org/ | ||||||
|  | [2]: https://qutebrowser.org/ | ||||||
|  | [3]: https://gnupg.org/ | ||||||
|  | [4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md | ||||||
|  | [5]: https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc | ||||||
|  | [6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import sys | ||||||
|  | import os | ||||||
|  | import socket | ||||||
|  | import json | ||||||
|  | import base64 | ||||||
|  | import shlex | ||||||
|  | import subprocess | ||||||
|  | import argparse | ||||||
|  | 
 | ||||||
|  | import nacl.utils | ||||||
|  | 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('--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', | ||||||
|  |                         help='GPG key to encrypt KeepassXC auth key with') | ||||||
|  |     parser.add_argument('--insecure', action='store_true', | ||||||
|  |                         help="Do not encrypt auth key") | ||||||
|  |     parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu', | ||||||
|  |                         help='Invocation used to execute a dmenu-provider') | ||||||
|  |     parser.add_argument('--only-username', action='store_true', | ||||||
|  |                         help='Only insert username') | ||||||
|  |     parser.add_argument('--only-password', action='store_true', | ||||||
|  |                         help='Only insert password') | ||||||
|  |     parser.add_argument('--only-otp', action='store_true', | ||||||
|  |                         help='Only insert OTP code') | ||||||
|  |     return parser.parse_args() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class KeepassError(Exception): | ||||||
|  |     def __init__(self, code, desc): | ||||||
|  |         self.code = code | ||||||
|  |         self.description = desc | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return f"KeepassXC Error [{self.code}]: {self.description}" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class KeepassXC: | ||||||
|  |     """ Wrapper around the KeepassXC socket API """ | ||||||
|  |     def __init__(self, id=None, *, key, socket_path): | ||||||
|  |         self.sock        = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||||
|  |         self.id          = id | ||||||
|  |         self.socket_path = socket_path | ||||||
|  |         self.client_key  = nacl.public.PrivateKey.generate() | ||||||
|  |         self.id_key      = nacl.public.PrivateKey.from_seed(key) | ||||||
|  |         self.cryptobox   = None | ||||||
|  | 
 | ||||||
|  |     def connect(self): | ||||||
|  |         if not os.path.exists(self.socket_path): | ||||||
|  |             raise KeepassError(-1, "KeepassXC Browser socket does not exists") | ||||||
|  |         self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8') | ||||||
|  |         self.sock.connect(self.socket_path) | ||||||
|  | 
 | ||||||
|  |         self.send_raw_msg(dict( | ||||||
|  |             action    = 'change-public-keys', | ||||||
|  |             publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'), | ||||||
|  |             nonce     = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'), | ||||||
|  |             clientID  = self.client_id | ||||||
|  |         )) | ||||||
|  | 
 | ||||||
|  |         resp = self.recv_raw_msg() | ||||||
|  |         assert resp['action'] == 'change-public-keys' | ||||||
|  |         assert resp['success'] == 'true' | ||||||
|  |         assert resp['nonce'] | ||||||
|  |         self.cryptobox = nacl.public.Box( | ||||||
|  |             self.client_key, | ||||||
|  |             nacl.public.PublicKey(base64.b64decode(resp['publicKey'])) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def get_databasehash(self): | ||||||
|  |         self.send_msg(dict(action='get-databasehash')) | ||||||
|  |         return self.recv_msg()['hash'] | ||||||
|  | 
 | ||||||
|  |     def lock_database(self): | ||||||
|  |         self.send_msg(dict(action='lock-database')) | ||||||
|  |         try: | ||||||
|  |             self.recv_msg() | ||||||
|  |         except KeepassError as e: | ||||||
|  |             if e.code == 1: | ||||||
|  |                 return True | ||||||
|  |             raise | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def test_associate(self): | ||||||
|  |         if not self.id: | ||||||
|  |             return False | ||||||
|  |         self.send_msg(dict( | ||||||
|  |             action = 'test-associate', | ||||||
|  |             id     = self.id, | ||||||
|  |             key    = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') | ||||||
|  |         )) | ||||||
|  |         return self.recv_msg()['success'] == 'true' | ||||||
|  | 
 | ||||||
|  |     def associate(self): | ||||||
|  |         self.send_msg(dict( | ||||||
|  |             action = 'associate', | ||||||
|  |             key    = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'), | ||||||
|  |             idKey  = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') | ||||||
|  |         )) | ||||||
|  |         resp = self.recv_msg() | ||||||
|  |         self.id = resp['id'] | ||||||
|  | 
 | ||||||
|  |     def get_logins(self, url): | ||||||
|  |         self.send_msg(dict( | ||||||
|  |             action = 'get-logins', | ||||||
|  |             url    = url, | ||||||
|  |             keys   = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }] | ||||||
|  |         )) | ||||||
|  |         return self.recv_msg()['entries'] | ||||||
|  | 
 | ||||||
|  |     def get_totp(self, uuid): | ||||||
|  |         self.send_msg(dict( | ||||||
|  |             action = 'get-totp', | ||||||
|  |             uuid = uuid, | ||||||
|  |         )) | ||||||
|  |         return self.recv_msg()['totp'] | ||||||
|  | 
 | ||||||
|  |     def send_raw_msg(self, msg): | ||||||
|  |         self.sock.send( json.dumps(msg).encode('utf-8') ) | ||||||
|  | 
 | ||||||
|  |     def recv_raw_msg(self): | ||||||
|  |         return json.loads( self.sock.recv(4096).decode('utf-8') ) | ||||||
|  | 
 | ||||||
|  |     def send_msg(self, msg, **extra): | ||||||
|  |         nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE) | ||||||
|  |         self.send_raw_msg(dict( | ||||||
|  |             action   = msg['action'], | ||||||
|  |             message  = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'), | ||||||
|  |             nonce    = base64.b64encode(nonce).decode('utf-8'), | ||||||
|  |             clientID = self.client_id, | ||||||
|  |             **extra | ||||||
|  |         )) | ||||||
|  | 
 | ||||||
|  |     def recv_msg(self): | ||||||
|  |         resp = self.recv_raw_msg() | ||||||
|  |         if 'error' in resp: | ||||||
|  |             raise KeepassError(resp['errorCode'], resp['error']) | ||||||
|  |         assert resp['action'] | ||||||
|  |         return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8')) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SecretKeyStore: | ||||||
|  |     def __init__(self, gpgkey): | ||||||
|  |         self.gpgkey = gpgkey | ||||||
|  |         if gpgkey is None: | ||||||
|  |             self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key') | ||||||
|  |         else: | ||||||
|  |             self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg') | ||||||
|  | 
 | ||||||
|  |     def load(self): | ||||||
|  |         "Load existing association key from file" | ||||||
|  |         if self.gpgkey is None: | ||||||
|  |             jsondata = open(self.path, 'r').read() | ||||||
|  |         else: | ||||||
|  |             jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8') | ||||||
|  |         data = json.loads(jsondata) | ||||||
|  |         self.id = data['id'] | ||||||
|  |         self.key = base64.b64decode(data['key']) | ||||||
|  | 
 | ||||||
|  |     def create(self): | ||||||
|  |         "Create new association key" | ||||||
|  |         self.key = nacl.utils.random(32) | ||||||
|  |         self.id = None | ||||||
|  | 
 | ||||||
|  |     def store(self, id): | ||||||
|  |         "Store newly created association key in file" | ||||||
|  |         self.id = id | ||||||
|  |         jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')}) | ||||||
|  |         if self.gpgkey is None: | ||||||
|  |             open(self.path, "w").write(jsondata) | ||||||
|  |         else: | ||||||
|  |             subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True) | ||||||
|  | 
 | ||||||
|  | def fake_key_raw(text): | ||||||
|  |     for character in text: | ||||||
|  |         # Escape all characters by default, space requires special handling | ||||||
|  |         sequence = '" "' if character == ' ' else '\{}'.format(character) | ||||||
|  |         qute('fake-key {}'.format(sequence)) | ||||||
|  | 
 | ||||||
|  | def qute(cmd): | ||||||
|  |     with open(os.environ['QUTE_FIFO'], 'w') as fifo: | ||||||
|  |         fifo.write(cmd) | ||||||
|  |         fifo.write('\n') | ||||||
|  |         fifo.flush() | ||||||
|  | 
 | ||||||
|  | def error(msg): | ||||||
|  |     print(msg, file=sys.stderr) | ||||||
|  |     qute('message-error "{}"'.format(msg)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def connect_to_keepassxc(args): | ||||||
|  |     assert args.key or args.insecure, "Missing GPG key to use for auth key encryption" | ||||||
|  |     keystore = SecretKeyStore(args.key) | ||||||
|  |     if os.path.isfile(keystore.path): | ||||||
|  |         keystore.load() | ||||||
|  |         kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket) | ||||||
|  |         kp.connect() | ||||||
|  |         if not kp.test_associate(): | ||||||
|  |             error('No KeepassXC association') | ||||||
|  |             return None | ||||||
|  |     else: | ||||||
|  |         keystore.create() | ||||||
|  |         kp = KeepassXC(key=keystore.key, socket_path=args.socket) | ||||||
|  |         kp.connect() | ||||||
|  |         kp.associate() | ||||||
|  |         if not kp.test_associate(): | ||||||
|  |             error('No KeepassXC association') | ||||||
|  |             return None | ||||||
|  |         keystore.store(kp.id) | ||||||
|  |     return kp | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def make_js_code(username, password): | ||||||
|  |     return ' '.join(""" | ||||||
|  |         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 = %s; | ||||||
|  |                     input.dispatchEvent(new Event('input', { 'bubbles': true })); | ||||||
|  |                     input.dispatchEvent(new Event('change', { 'bubbles': true })); | ||||||
|  |                     input.blur(); | ||||||
|  |                 } | ||||||
|  |                 if (input.type === "password") { | ||||||
|  |                     input.focus(); | ||||||
|  |                     input.value = %s; | ||||||
|  |                     input.dispatchEvent(new Event('input', { 'bubbles': true })); | ||||||
|  |                     input.dispatchEvent(new Event('change', { 'bubbles': true })); | ||||||
|  |                     input.blur(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         function fillFirstForm() { | ||||||
|  |             var forms = document.getElementsByTagName("form"); | ||||||
|  |             for (i = 0; i < forms.length; i++) { | ||||||
|  |                 if (hasPasswordField(forms[i])) { | ||||||
|  |                     loadData2Form(forms[i]); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             alert("No Credentials Form found"); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         fillFirstForm() | ||||||
|  |     """.splitlines()) % (json.dumps(username), json.dumps(password)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def dmenu(items, invocation): | ||||||
|  |     command = shlex.split(invocation) | ||||||
|  |     process = subprocess.run(command, input='\n'.join(items) | ||||||
|  |                              .encode('UTF-8'), stdout=subprocess.PIPE) | ||||||
|  |     return process.stdout.decode('UTF-8').strip() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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") | ||||||
|  |         sys.exit(-1) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         args = parse_args() | ||||||
|  |         assert args.url, "Missing URL" | ||||||
|  |         kp = connect_to_keepassxc(args) | ||||||
|  |         if not kp: | ||||||
|  |             error('Could not connect to KeepassXC') | ||||||
|  |             return | ||||||
|  |         creds = kp.get_logins(args.url) | ||||||
|  |         if not creds: | ||||||
|  |             error('No credentials found') | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         selection = creds[0] | ||||||
|  |         if len(creds) > 1: | ||||||
|  |             login = dmenu(sorted(map(lambda c: c['login'], creds)), args.dmenu_invocation) | ||||||
|  | 
 | ||||||
|  |             for c in creds: | ||||||
|  |                 if c['login'] == login: | ||||||
|  |                     selection = c | ||||||
|  |                     break | ||||||
|  | 
 | ||||||
|  |         name, pw = selection['login'], selection['password'] | ||||||
|  | 
 | ||||||
|  |         if args.only_username: | ||||||
|  |             if name: | ||||||
|  |                 fake_key_raw(name) | ||||||
|  |             return | ||||||
|  |         if args.only_password: | ||||||
|  |             if pw: | ||||||
|  |                 fake_key_raw(pw) | ||||||
|  |             return | ||||||
|  |         if args.only_otp: | ||||||
|  |             otp = kp.get_totp(selection['uuid']) | ||||||
|  |             if otp: | ||||||
|  |                 fake_key_raw(otp) | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         if name and pw: | ||||||
|  |             qute('jseval -q ' + make_js_code(name, pw)) | ||||||
|  |     except Exception as e: | ||||||
|  |         error(str(e)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     main() | ||||||
|  | 
 | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue