diff --git a/browser_cli/auth.py b/browser_cli/auth.py index 1b424b6..97f956e 100644 --- a/browser_cli/auth.py +++ b/browser_cli/auth.py @@ -182,22 +182,33 @@ def new_nonce() -> str: return secrets.token_hex(32) -def load_authorized_keys(path: Path) -> list[str]: +def load_authorized_keys_with_names(path: Path) -> list[tuple[str, str]]: + """Return list of (pubkey_hex, name) pairs. Name is empty string if not set.""" if not path.exists(): return [] - return [ - line.strip() - for line in path.read_text(encoding="utf-8").splitlines() - if line.strip() and not line.startswith("#") - ] + result = [] + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split(None, 1) + pubkey = parts[0] + name = parts[1].strip() if len(parts) > 1 else "" + result.append((pubkey, name)) + return result -def add_authorized_key(path: Path, pub_hex: str) -> bool: +def load_authorized_keys(path: Path) -> list[str]: + return [pk for pk, _ in load_authorized_keys_with_names(path)] + + +def add_authorized_key(path: Path, pub_hex: str, name: str = "") -> bool: """Append pub_hex to authorized_keys. Returns False if already present.""" path.parent.mkdir(parents=True, exist_ok=True) - existing = set(load_authorized_keys(path)) + existing = {pk for pk, _ in load_authorized_keys_with_names(path)} if pub_hex in existing: return False + line = (f"{pub_hex} {name}".rstrip()) + "\n" with open(path, "a", encoding="utf-8") as f: - f.write(pub_hex + "\n") + f.write(line) return True diff --git a/browser_cli/cli.py b/browser_cli/cli.py index ac7e798..99b779a 100755 --- a/browser_cli/cli.py +++ b/browser_cli/cli.py @@ -252,9 +252,11 @@ def cmd_auth_keygen(output, force): @auth_group.command("trust") @click.argument("pubkey") +@click.option("--name", default="", metavar="NAME", help="Human-friendly label for this key.") @click.option("--file", "keys_file", default=None, metavar="PATH", help="Authorized keys file (default: ~/.config/browser-cli/authorized_keys).") -def cmd_auth_trust(pubkey, keys_file): - """Add a public key to the authorized keys file on the serve host.""" +@click.pass_context +def cmd_auth_trust(ctx, pubkey, name, keys_file): + """Add a public key to the authorized keys file (locally or on a remote serve host).""" from browser_cli.auth import DEFAULT_AUTHORIZED_KEYS_PATH, add_authorized_key if len(pubkey) != 64: @@ -266,10 +268,28 @@ def cmd_auth_trust(pubkey, keys_file): console.print("[red]Invalid public key:[/red] not valid hex") sys.exit(1) + remote = (ctx.obj or {}).get("remote") + if remote: + from browser_cli.client import send_command + result = send_command( + "browser-cli.auth.trust", + args={"pubkey": pubkey, "name": name}, + remote=remote, + key=(ctx.obj or {}).get("key"), + ) + added = (result or {}).get("added", False) + label = f" ({name})" if name else "" + if added: + console.print(f"[green]✓[/green] Trusted on {remote}{label}: [cyan]{pubkey}[/cyan]") + else: + console.print(f"[yellow]Already trusted on {remote}:[/yellow] {pubkey}") + return + path = Path(keys_file) if keys_file else DEFAULT_AUTHORIZED_KEYS_PATH - added = add_authorized_key(path, pubkey) + added = add_authorized_key(path, pubkey, name) + label = f" ({name})" if name else "" if added: - console.print(f"[green]✓[/green] Trusted: [cyan]{pubkey}[/cyan]") + console.print(f"[green]✓[/green] Trusted{label}: [cyan]{pubkey}[/cyan]") console.print(f" File: {path}") console.print(f"\nStart the server with:") console.print(f" [dim]browser-cli serve --authorized-keys {path}[/dim]") @@ -313,39 +333,40 @@ def cmd_auth_show(key_src): @auth_group.command("keys") -def cmd_auth_keys(): - """List all Ed25519 keys available for pubkey auth (file + SSH agent).""" - from browser_cli.auth import DEFAULT_KEY_PATH, agent_list_keys, load_private_key, public_key_hex +@click.option("--file", "keys_file", default=None, metavar="PATH", help="Authorized keys file (default: ~/.config/browser-cli/authorized_keys).") +@click.pass_context +def cmd_auth_keys(ctx, keys_file): + """List trusted public keys (server's authorized_keys). With --remote, queries the remote server.""" from rich.table import Table - table = Table(show_header=True, header_style="bold cyan") - table.add_column("Source") - table.add_column("Comment / Path") - table.add_column("Public Key") + remote = (ctx.obj or {}).get("remote") + if remote: + from browser_cli.client import send_command + result = send_command( + "browser-cli.auth.keys", + remote=remote, + key=(ctx.obj or {}).get("key"), + ) + entries = result or [] + source_label = remote + else: + from browser_cli.auth import DEFAULT_AUTHORIZED_KEYS_PATH, load_authorized_keys_with_names + path = Path(keys_file) if keys_file else DEFAULT_AUTHORIZED_KEYS_PATH + entries = [{"pubkey": pk, "name": name} for pk, name in load_authorized_keys_with_names(path)] + source_label = str(path) - # File key - if DEFAULT_KEY_PATH.exists(): - try: - priv = load_private_key(DEFAULT_KEY_PATH) - hex_key = public_key_hex(priv) - table.add_row("[green]file[/green]", str(DEFAULT_KEY_PATH), hex_key) - except Exception as e: - table.add_row("[red]file[/red]", str(DEFAULT_KEY_PATH), f"[red]{e}[/red]") - - # Agent keys - try: - agent_keys = agent_list_keys() - for k in agent_keys: - table.add_row("[cyan]agent[/cyan]", k.comment, public_key_hex(k)) - except Exception as e: - table.add_row("[dim]agent[/dim]", f"[dim]{e}[/dim]", "") - - if table.row_count == 0: - console.print("[yellow]No keys found.[/yellow] Run: [dim]browser-cli auth keygen[/dim]") + if not entries: + console.print(f"[yellow]No trusted keys[/yellow] in {source_label}") + console.print(" Add one: [dim]browser-cli auth trust --name