no nonsence CLI wrapper for bw
This commit is contained in:
commit
d6545b2c6c
1 changed files with 168 additions and 0 deletions
168
cli/bww.py
Executable file
168
cli/bww.py
Executable file
|
|
@ -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 <key>\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 <key>", 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()
|
||||||
Loading…
Reference in a new issue