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>
This commit is contained in:
Srijan Choudhary 2022-10-28 13:44:56 +05:30 committed by GitHub
parent 63afed98d3
commit 88e1aa6027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 35 deletions

View File

@ -8,6 +8,7 @@ Qutebrowser userscript to fill 1password credentials
- [The 1Password CLI](https://support.1password.com/command-line-getting-started/) - [The 1Password CLI](https://support.1password.com/command-line-getting-started/)
Ensure you have it installed and set up. Follow the official documentation. 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 - [rofi](https://github.com/davatorium/rofi) to ask for password and list items
## How it works ## 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. - `--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. - `--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 - `--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 $ 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: positional arguments:
command fill_credentials, fill_totp, fill_username, fill_password command fill_credentials, fill_totp, fill_username, fill_password
optional arguments: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--auto-submit Auto submit after filling --auto-submit Auto submit after filling
--cache-session Cache 1password session for 30 minutes --cache-session Cache 1password session for 30 minutes
--allow-insecure-sites --allow-insecure-sites
Allow filling credentials on 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 Call your script from qutebrowser using

View File

@ -22,17 +22,17 @@ SESSION_DURATION = timedelta(minutes=30)
LAST_ITEM_PATH = os.path.join(CACHE_DIR, "last_item") LAST_ITEM_PATH = os.path.join(CACHE_DIR, "last_item")
LAST_ITEM_DURATION = timedelta(seconds=10) LAST_ITEM_DURATION = timedelta(seconds=10)
OP_SUBDOMAIN = "my"
CMD_PASSWORD_PROMPT = [ CMD_PASSWORD_PROMPT = [
"rofi", "-password", "-dmenu", "-p", "Vault Password", "-l", "0", "-sidebar", "-width", "20" "rofi", "-password", "-dmenu", "-p", "Vault Password", "-l", "0", "-sidebar", "-width", "20"
] ]
CMD_LIST_PROMPT = ["rofi", "-dmenu"] CMD_LIST_PROMPT = ["rofi", "-dmenu"]
CMD_ITEM_SELECT = CMD_LIST_PROMPT + ["-p", "Select login"] CMD_ITEM_SELECT = CMD_LIST_PROMPT + ["-p", "Select login"]
CMD_OP_LOGIN = ["op", "signin", "--output=raw"] CMD_OP_CHECK_LOGIN = ["op", "whoami"]
CMD_OP_LIST_ITEMS = "op list items --categories Login --session {session_id}" CMD_OP_LOGIN = ["op", "signin", "--raw"]
CMD_OP_GET_ITEM = "op get item --session {session_id} {uuid}" CMD_OP_LIST_ITEMS = "op item list --categories Login --session {session_id} --format=json"
CMD_OP_GET_TOTP = "op get totp --session {session_id} {uuid}" 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"] QUTE_FIFO = os.environ["QUTE_FIFO"]
@ -58,6 +58,11 @@ parser.add_argument(
help="store and use cached information", help="store and use cached information",
action="store_true", action="store_true",
) )
parser.add_argument(
"--biometric",
help="Use biometric unlock - don't ask for password",
action="store_true",
)
class Qute: class Qute:
@ -144,26 +149,35 @@ class OnePass:
@classmethod @classmethod
def login(cls): def login(cls):
try: if arguments.biometric:
password = execute_command(CMD_PASSWORD_PROMPT) try:
except ExecuteError: execute_command(CMD_OP_CHECK_LOGIN)
Qute.message_error("Error calling pinentry program") except ExecuteError:
sys.exit(0) 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: if arguments.cache_session:
session_id = pipe_commands( with open(SESSION_PATH, "w") as handler:
["echo", "-n", password], handler.write(session_id)
CMD_OP_LOGIN + [OP_SUBDOMAIN]) os.chmod(SESSION_PATH, 0o640)
except ExecuteError: return session_id
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
@classmethod @classmethod
def get_session(cls): def get_session(cls):
@ -208,14 +222,14 @@ class OnePass:
def filter_host(item): def filter_host(item):
"""Exclude items that does not match host on any configured URL""" """Exclude items that does not match host on any configured URL"""
if "URLs" in item["overview"]: if "urls" in item:
return any(filter(lambda x: host in x["u"], item["overview"]["URLs"])) return any(filter(lambda x: host in x["href"], item["urls"]))
return False return False
items = cls.list_items() items = cls.list_items()
filtered = filter(filter_host, items) filtered = filter(filter_host, items)
mapping = { mapping = {
f"{host}: {item['overview']['title']} ({item['uuid']})": item f"{host}: {item['title']} ({item['id']})": item
for item in filtered for item in filtered
} }
@ -233,15 +247,15 @@ class OnePass:
# Cancelled # Cancelled
return return
return cls.get_item(mapping[credential]["uuid"]) return cls.get_item(mapping[credential]["id"])
@classmethod @classmethod
def get_credentials(cls, item): def get_credentials(cls, item):
username = password = None username = password = None
for field in item["details"]["fields"]: for field in item["fields"]:
if field.get("designation") == "username": if field.get("purpose") == "USERNAME":
username = field["value"] username = field["value"]
if field.get("designation") == "password": if field.get("purpose") == "PASSWORD":
password = field["value"] password = field["value"]
if username is None or password is None: 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) Stores a reference to an item to easily get single information from it (password, TOTP)
right after filling the username or credentials. 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: with open(LAST_ITEM_PATH, "w") as handler:
handler.write(json.dumps(last_item)) handler.write(json.dumps(last_item))
os.chmod(LAST_ITEM_PATH, 0o640) os.chmod(LAST_ITEM_PATH, 0o640)
@ -338,7 +352,7 @@ class CLI:
if not item: if not item:
item = self._get_item() item = self._get_item()
totp = OnePass.get_totp(item["uuid"]) totp = OnePass.get_totp(item["id"])
logger.error(totp) logger.error(totp)
Qute.fill_totp(totp) Qute.fill_totp(totp)