commit d6545b2c6c69b33ec8490c32f67aa9332877772d Author: Jean-Michel Tremblay Date: Wed Apr 22 21:46:14 2026 -0400 no nonsence CLI wrapper for bw diff --git a/cli/bww.py b/cli/bww.py new file mode 100755 index 0000000..84905ca --- /dev/null +++ b/cli/bww.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +import json +import os +import subprocess +import sys +import getpass + +BW_EMAIL = "jeanmi.tremblay@gmail.com" +BW_PASS_PATH = "vaultwarden/jeanmi.tremblay@gmail.com/login" +BW_TOTP_PATH = "vaultwarden/jeanmi.tremblay@gmail.com/authenticator" + + +def run(cmd, input=None, check=True): + result = subprocess.run(cmd, input=input, capture_output=True, text=True) + if check and result.returncode != 0: + print(result.stderr.strip(), file=sys.stderr) + sys.exit(1) + return result.stdout.strip() + + +import tempfile + +SESSION_FILE = os.path.join(tempfile.gettempdir(), f"bw_session_{os.getuid()}") + +def save_session(session): + with open(SESSION_FILE, "w") as f: + f.write(session) + os.chmod(SESSION_FILE, 0o600) + +def load_session(): + try: + with open(SESSION_FILE) as f: + return f.read().strip() + except FileNotFoundError: + return "" + +def ensure_session(): + session = load_session() or os.environ.get("BW_SESSION", "") + if session: + result = subprocess.run( + ["bw", "unlock", "--check"], + capture_output=True, + env={**os.environ, "BW_SESSION": session} + ) + if result.returncode == 0: + os.environ["BW_SESSION"] = session + return session + + print("Touch your YubiKey to decrypt password...") + password = run(["pass", "show", BW_PASS_PATH]) + + print("Touch your YubiKey to decrypt TOTP secret...") + totp_secret = run(["pass", "show", BW_TOTP_PATH]) + totp = run(["oathtool", "--totp", "-b", totp_secret]) + + print("Authenticating...") + env = {**os.environ, "BW_PASSWORD": password} + + session = subprocess.run( + ["bw", "login", BW_EMAIL, "--passwordenv", "BW_PASSWORD", "--code", totp, "--raw"], + capture_output=True, text=True, env=env + ).stdout.strip() + + if not session: + session = subprocess.run( + ["bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw"], + capture_output=True, text=True, env=env + ).stdout.strip() + + if not session: + print("Failed to get session.", file=sys.stderr) + sys.exit(1) + + save_session(session) + os.environ["BW_SESSION"] = session + return session + + +def get_id(key, session): + items = json.loads(run(["bw", "list", "items", "--search", key, "--session", session])) + matches = [i for i in items if i["name"] == key] + return matches[0]["id"] if matches else None + + +def cmd_insert(key, session): + val = getpass.getpass("Secret: ") + val2 = getpass.getpass("Secret (again): ") + if val != val2: + print("Secrets do not match.", file=sys.stderr) + sys.exit(1) + + item_id = get_id(key, session) + if item_id: + item = json.loads(run(["bw", "get", "item", item_id, "--session", session])) + item["login"]["password"] = val + encoded = run(["bw", "encode"], input=json.dumps(item)) + run(["bw", "edit", "item", item_id, "--session", session], input=encoded) + else: + item = {"type": 1, "name": key, "login": {"username": "", "password": val}} + encoded = run(["bw", "encode"], input=json.dumps(item)) + run(["bw", "create", "item", "--session", session], input=encoded) + + print("Saved.") + + +def cmd_generate(key, session): + val = run(["bw", "generate", "-ulns", "--length", "32"]) + item = {"type": 1, "name": key, "login": {"username": "", "password": val}} + encoded = run(["bw", "encode"], input=json.dumps(item)) + run(["bw", "create", "item", "--session", session], input=encoded) + print(val) + + +def cmd_show(key, session): + result = subprocess.run( + ["bw", "get", "password", key, "--session", session], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f"Not found: {key}", file=sys.stderr) + sys.exit(1) + print(result.stdout.strip()) + + +def print_help(): + print("Usage: bww.py insert|generate|show \n") + print("Examples:") + print(" bww.py insert foo") + print(" Prompt for a secret (twice). Creates or updates the Vaultwarden item named 'foo' with the provided password.") + print("") + print(" bww.py show bar") + print(" Retrieves and prints the password for item 'bar' to stdout. Exits with error if not found.") + print("") + print(" bww.py generate foobar") + print(" Generates a 32-character password, stores it as the password for item 'foobar', and prints the generated value.") + print("") + print("Notes:") + print(" - The script reuses an unlocked Bitwarden session stored in a temp file or the BW_SESSION environment variable.") + print(" - If no valid session exists the script will attempt to unlock/login using values from your local 'pass' store and an oathtool TOTP code (it may prompt you to touch your YubiKey).") + print(" - Secrets and generated passwords are written to Vaultwarden via the 'bw' CLI.") + +def main(): + # Support a simple help flag + if len(sys.argv) >= 2 and sys.argv[1] in ("-h", "--help"): + print_help() + sys.exit(0) + + if len(sys.argv) < 3: + print("Usage: bww insert|generate|show ", file=sys.stderr) + sys.exit(1) + + cmd = sys.argv[1] + key = sys.argv[2] + session = ensure_session() + + if cmd == "insert": + cmd_insert(key, session) + elif cmd == "generate": + cmd_generate(key, session) + elif cmd == "show": + cmd_show(key, session) + else: + print(f"Unknown command: {cmd}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file