Add rbw integration for automated Google login

- Look up credentials from rbw password manager
- Automate email/password entry in Google login flow
- Add --copy flag to copy cookie JSON to clipboard
- Add --entry flag to specify rbw entry name
- Add --manual flag to skip auto-login
- Update flake.nix with rbw, wl-clipboard, xclip deps
This commit is contained in:
2026-01-18 22:22:26 -08:00
committed by John Ogle
parent a1f6956657
commit 9ebd2c4b6d
2 changed files with 189 additions and 51 deletions

View File

@@ -12,6 +12,9 @@
pkgs.python3Packages.selenium
pkgs.chromedriver
pkgs.chromium
pkgs.rbw
pkgs.wl-clipboard
pkgs.xclip
];
in {
devShells.${system}.default = pkgs.mkShell {

View File

@@ -1,69 +1,204 @@
#!/usr/bin/env python3
import time
import argparse
import json
import subprocess
import sys
import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
# Configure Chrome options to run in incognito mode.
options = webdriver.ChromeOptions()
options.add_argument("--incognito")
# Uncomment the following option if you want to auto-open developer tools for each tab.
# options.add_argument("--auto-open-devtools-for-tabs")
# Initialize the Chrome driver.
# If ChromeDriver is not in your PATH, specify its location: Service('/path/to/chromedriver')
service = Service() # Assumes chromedriver is in your PATH
def check_rbw_unlocked():
"""Check if rbw vault is unlocked."""
result = subprocess.run(["rbw", "unlocked"], capture_output=True)
return result.returncode == 0
driver = webdriver.Chrome(service=service, options=options)
print("Opening https://chat.google.com ...")
driver.get("https://chat.google.com")
def get_rbw_credentials(entry: str):
"""Get username and password from rbw."""
# Get password
result = subprocess.run(
["rbw", "get", entry],
capture_output=True,
text=True,
)
if result.returncode != 0:
return None, None
print("\nA new incognito window has been opened.")
print("Please log in normally. To inspect cookies manually:")
print(" 1. Press F12 to open developer tools.")
print(" 2. Navigate to the Application (Chrome) or Storage (Firefox) tab.")
print(" 3. Expand Cookies and select https://chat.google.com.")
print(" 4. Verify that the COMPASS, SSID, SID, OSID, and HSID cookies are present.")
print("\nIMPORTANT: Once you are logged in and have confirmed the cookies (or just logged in),")
print("press Enter here. (Remember: to keep the validity of these cookies, close the browser soon!)")
input("Press Enter to extract cookies...")
password = result.stdout.strip()
# Navigate to some invalid URL on chat.google.com. This way we won't be redirected back to a
# mail.google.com page and this allows us to extract the correct cookies.
driver.get("https://chat.google.com/u/0/mole/world")
# Get username
result = subprocess.run(
["rbw", "get", "--field", "username", entry],
capture_output=True,
text=True,
)
username = result.stdout.strip() if result.returncode == 0 else None
# Get all cookies
all_cookies = driver.get_cookies()
return username, password
# Define the cookie names we want (case-insensitive)
target_names = {"COMPASS", "SSID", "SID", "OSID", "HSID"}
extracted = {}
# For COMPASS cookie, if multiple are present, prefer the one with path == "/"
compass_cookie = None
def copy_to_clipboard(text: str):
"""Copy text to clipboard using wl-copy or xclip."""
# Try wl-copy first (Wayland)
try:
subprocess.run(
["wl-copy"],
input=text.encode(),
check=True,
capture_output=True,
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
pass
for cookie in all_cookies:
name_upper = cookie["name"].upper()
if name_upper not in target_names:
continue
if name_upper == "COMPASS":
if cookie.get("path", "") == "/":
compass_cookie = cookie
elif compass_cookie is None:
compass_cookie = cookie
else:
extracted[name_upper] = cookie["value"]
# Fall back to xclip (X11)
try:
subprocess.run(
["xclip", "-selection", "clipboard"],
input=text.encode(),
check=True,
capture_output=True,
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
pass
if compass_cookie:
extracted["COMPASS"] = compass_cookie["value"]
return False
# Form JSON object of the extracted cookies.
json_data = json.dumps(extracted, indent=2)
print("\nExtracted Cookie JSON:")
print(json_data)
print("\nClosing the browser window to avoid invalidating the cookies (Google uses refresh tokens).")
def automate_google_login(driver, username: str, password: str):
"""Attempt to automate Google login. Returns True if successful."""
wait = WebDriverWait(driver, 10)
# Close the browser window.
driver.quit()
try:
# Wait for and fill email field
email_field = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'input[type="email"]'))
)
email_field.send_keys(username)
email_field.send_keys(Keys.RETURN)
# Wait for password field
time.sleep(2) # Google has animation between screens
password_field = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'input[type="password"]'))
)
password_field.send_keys(password)
password_field.send_keys(Keys.RETURN)
return True
except Exception as e:
print(f"Auto-login failed: {e}")
return False
def extract_cookies(driver):
"""Extract the required cookies from the browser."""
# Navigate to a chat.google.com path to ensure we get the right cookies
driver.get("https://chat.google.com/u/0/mole/world")
all_cookies = driver.get_cookies()
target_names = {"COMPASS", "SSID", "SID", "OSID", "HSID"}
extracted = {}
compass_cookie = None
for cookie in all_cookies:
name_upper = cookie["name"].upper()
if name_upper not in target_names:
continue
if name_upper == "COMPASS":
if cookie.get("path", "") == "/":
compass_cookie = cookie
elif compass_cookie is None:
compass_cookie = cookie
else:
extracted[name_upper] = cookie["value"]
if compass_cookie:
extracted["COMPASS"] = compass_cookie["value"]
return extracted
def main():
parser = argparse.ArgumentParser(
description="Extract Google Chat cookies for Mautrix bridge authentication"
)
parser.add_argument(
"--entry",
default="google.com",
help="rbw entry name for Google credentials (default: google.com)",
)
parser.add_argument(
"--copy",
action="store_true",
help="Copy the cookie JSON to clipboard",
)
parser.add_argument(
"--manual",
action="store_true",
help="Skip auto-login, do manual login only",
)
args = parser.parse_args()
username = None
password = None
# Get credentials from rbw if not manual mode
if not args.manual:
if not check_rbw_unlocked():
print("rbw vault is locked. Please run: rbw unlock")
sys.exit(1)
username, password = get_rbw_credentials(args.entry)
if not username or not password:
print(f"Could not get credentials from rbw entry '{args.entry}'")
print("Falling back to manual login...")
# Configure Chrome options
options = webdriver.ChromeOptions()
options.add_argument("--incognito")
# Initialize the Chrome driver
service = Service()
driver = webdriver.Chrome(service=service, options=options)
try:
print("Opening https://chat.google.com ...")
driver.get("https://chat.google.com")
if username and password and not args.manual:
print("Attempting automatic login...")
if automate_google_login(driver, username, password):
print("Credentials entered. Complete any 2FA if prompted.")
else:
print("Auto-login failed, please log in manually.")
print("\nOnce logged in, press Enter to extract cookies...")
input()
cookies = extract_cookies(driver)
json_data = json.dumps(cookies, indent=2)
print("\nExtracted Cookie JSON:")
print(json_data)
if args.copy:
if copy_to_clipboard(json_data):
print("\nCopied to clipboard!")
else:
print("\nFailed to copy to clipboard (wl-copy/xclip not found)")
finally:
print("\nClosing browser...")
driver.quit()
if __name__ == "__main__":
main()