From 88e1aa602703ee4caa4ded37fc18f56ff338ceac Mon Sep 17 00:00:00 2001 From: Srijan Choudhary Date: Fri, 28 Oct 2022 13:44:56 +0530 Subject: [PATCH] Move to 1Password CLI 2, and support biometric authentication (#9) * Migrate to 1Password CLI 2 V2 of the 1Password CLI is not backwards compatible: https://developer.1password.com/docs/cli/upgrade/#step-2-update-your-scripts * Support biometric * Update README * Update README.md Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> --- README.md | 10 +++++-- qute_1pass.py | 80 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index b356173..2b096d5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Qutebrowser userscript to fill 1password credentials - [The 1Password CLI](https://support.1password.com/command-line-getting-started/) Ensure you have it installed and set up. Follow the official documentation. +> ℹ️ **Note**: Only the 1Password CLI v2 is supported. - [rofi](https://github.com/davatorium/rofi) to ask for password and list items ## How it works @@ -34,20 +35,25 @@ Flags: - `--auto-submit` Will send a carriage return once the last character is sent, hopefully submitting the form. - `--cache-session` Caches the session for 30 minutes to prevent asking for the password again in that interval. - `--allow-insecure-sites` Allow filling in insecure (non-https) sites +- `--biometric` Use biometric or PAM authentication instead of asking for the master password + +Using the biometric flag requires installing the 1Password Desktop app and enabling "Biometric unlock" in it's Developer options. ``` $ python qute_1pass.py --help -usage: qute_1pass.py [-h] [--auto-submit] [--cache-session] [--allow-insecure-sites] command +usage: qute_1pass.py [-h] [--auto-submit] [--cache-session] [--allow-insecure-sites] [--cache] [--biometric] command positional arguments: command fill_credentials, fill_totp, fill_username, fill_password -optional arguments: +options: -h, --help show this help message and exit --auto-submit Auto submit after filling --cache-session Cache 1password session for 30 minutes --allow-insecure-sites Allow filling credentials on insecure sites + --cache store and use cached information + --biometric Use biometric unlock - don't ask for password ``` Call your script from qutebrowser using diff --git a/qute_1pass.py b/qute_1pass.py index 5ed9737..1302bef 100755 --- a/qute_1pass.py +++ b/qute_1pass.py @@ -22,17 +22,17 @@ SESSION_DURATION = timedelta(minutes=30) LAST_ITEM_PATH = os.path.join(CACHE_DIR, "last_item") LAST_ITEM_DURATION = timedelta(seconds=10) -OP_SUBDOMAIN = "my" CMD_PASSWORD_PROMPT = [ "rofi", "-password", "-dmenu", "-p", "Vault Password", "-l", "0", "-sidebar", "-width", "20" ] CMD_LIST_PROMPT = ["rofi", "-dmenu"] CMD_ITEM_SELECT = CMD_LIST_PROMPT + ["-p", "Select login"] -CMD_OP_LOGIN = ["op", "signin", "--output=raw"] -CMD_OP_LIST_ITEMS = "op list items --categories Login --session {session_id}" -CMD_OP_GET_ITEM = "op get item --session {session_id} {uuid}" -CMD_OP_GET_TOTP = "op get totp --session {session_id} {uuid}" +CMD_OP_CHECK_LOGIN = ["op", "whoami"] +CMD_OP_LOGIN = ["op", "signin", "--raw"] +CMD_OP_LIST_ITEMS = "op item list --categories Login --session {session_id} --format=json" +CMD_OP_GET_ITEM = "op item get --session {session_id} {uuid} --format=json" +CMD_OP_GET_TOTP = "op item get --otp --session {session_id} {uuid}" QUTE_FIFO = os.environ["QUTE_FIFO"] @@ -58,6 +58,11 @@ parser.add_argument( help="store and use cached information", action="store_true", ) +parser.add_argument( + "--biometric", + help="Use biometric unlock - don't ask for password", + action="store_true", +) class Qute: @@ -144,26 +149,35 @@ class OnePass: @classmethod def login(cls): - try: - password = execute_command(CMD_PASSWORD_PROMPT) - except ExecuteError: - Qute.message_error("Error calling pinentry program") - sys.exit(0) + if arguments.biometric: + try: + execute_command(CMD_OP_CHECK_LOGIN) + except ExecuteError: + try: + execute_command(CMD_OP_LOGIN) + except ExecuteError: + Qute.message_error("Login error") + sys.exit(0) + return "0" + else: + try: + password = execute_command(CMD_PASSWORD_PROMPT) + except ExecuteError: + Qute.message_error("Error calling pinentry program") + sys.exit(0) + try: + session_id = pipe_commands( + ["echo", "-n", password], + CMD_OP_LOGIN) + except ExecuteError: + Qute.message_error("Login error") + sys.exit(0) - try: - session_id = pipe_commands( - ["echo", "-n", password], - CMD_OP_LOGIN + [OP_SUBDOMAIN]) - except ExecuteError: - Qute.message_error("Login error") - sys.exit(0) - - if arguments.cache_session: - with open(SESSION_PATH, "w") as handler: - handler.write(session_id) - os.chmod(SESSION_PATH, 0o640) - - return session_id + if arguments.cache_session: + with open(SESSION_PATH, "w") as handler: + handler.write(session_id) + os.chmod(SESSION_PATH, 0o640) + return session_id @classmethod def get_session(cls): @@ -208,14 +222,14 @@ class OnePass: def filter_host(item): """Exclude items that does not match host on any configured URL""" - if "URLs" in item["overview"]: - return any(filter(lambda x: host in x["u"], item["overview"]["URLs"])) + if "urls" in item: + return any(filter(lambda x: host in x["href"], item["urls"])) return False items = cls.list_items() filtered = filter(filter_host, items) mapping = { - f"{host}: {item['overview']['title']} ({item['uuid']})": item + f"{host}: {item['title']} ({item['id']})": item for item in filtered } @@ -233,15 +247,15 @@ class OnePass: # Cancelled return - return cls.get_item(mapping[credential]["uuid"]) + return cls.get_item(mapping[credential]["id"]) @classmethod def get_credentials(cls, item): username = password = None - for field in item["details"]["fields"]: - if field.get("designation") == "username": + for field in item["fields"]: + if field.get("purpose") == "USERNAME": username = field["value"] - if field.get("designation") == "password": + if field.get("purpose") == "PASSWORD": password = field["value"] if username is None or password is None: @@ -292,7 +306,7 @@ class CLI: Stores a reference to an item to easily get single information from it (password, TOTP) right after filling the username or credentials. """ - last_item = {"host": extract_host(os.environ["QUTE_URL"]), "uuid": item["uuid"]} + last_item = {"host": extract_host(os.environ["QUTE_URL"]), "id": item["id"]} with open(LAST_ITEM_PATH, "w") as handler: handler.write(json.dumps(last_item)) os.chmod(LAST_ITEM_PATH, 0o640) @@ -338,7 +352,7 @@ class CLI: if not item: item = self._get_item() - totp = OnePass.get_totp(item["uuid"]) + totp = OnePass.get_totp(item["id"]) logger.error(totp) Qute.fill_totp(totp)