Skill: bieggerm/passwordstore-broker (version 1.1.1)

--- name: passwordstore-broker description: Enforce safe secret handling by collecting secrets through one-time HTTPS forms, storing them in pass via scripts/vault.sh, and executing tools with environment injection via scripts/run_with_secret.sh so raw secrets do not enter chat context or logs. metadata: compatibility: Requires pass, gpg, openssl, python3, and qrencode; local HTTPS network access is required, private LAN access is optional for phone flow. --- # Passwordstore Broker Agent Protocol Run this workflow whenever credentials are needed. ## Prerequisites - Follow `references/SETUP.md` before first use. ## Setup Preflight Before first LAN-mode intake, verify both files exist: - `~/.passwordstore-broker/totp.secret` - `~/.passwordstore-broker/setup_completed_at.txt` - If missing, run `scripts/setup_totp_enrollment.py` and send: - QR image at `qr_png_path` (preferred) - fallback `otpauth_url` - Record and trust `setup_completed_at` as the initial enrollment timestamp. - Never reveal or retransmit the `totp.secret` value after initial enrollment under any circumstances. - Do not rotate `totp.secret`. User has to do it manually if compromised. Rotation is not to be done by the agent. ## Phase 1: Get Secrets Goal: ensure required secrets exist in local vault without exposing values in chat. 1. Map auth requirements to `secret-name -> ENV_VAR`. 2. Check whether each secret exists: - `scripts/vault.sh exists <secret-name>` 3. If missing, collect via one-time HTTPS intake: - Local mode (default): - `scripts/get_password_from_user.py --secretname <secret-name> --port <port>` - LAN mode (when user asks for phone/private-network flow): - `scripts/get_password_from_user.py --secretname <secret-name> --port <port> --access lan` 4. Send generated intake URL to user. 5. In LAN mode, instruct user to submit both fields in the form: - secret value - current authenticator code 6. If intake fails or times out, retry with a new port. Exit criteria: - Required secret paths exist in vault. ## Phase 2: Use Secrets Goal: execute authenticated commands without exposing secret values. 1. Prefer injector wrapper: - `scripts/run_with_secret.sh --secret <secret-name> --env <ENV_VAR> -- <command> [args...]` 2. Fallback one-liner: - `<ENV_VAR>="$(scripts/vault.sh get <secret-name>)" <command> [args...]` 3. Never print env dumps (`env`, `printenv`, `set`) in secret-bearing runs. Exit criteria: - Authenticated command succeeds without secret leakage. ## Phase 3: Interact With Vault Goal: manage lifecycle safely. - Put/update: `scripts/vault.sh put <secret-name>` - Get (only when necessary): `scripts/vault.sh get <secret-name>` - Exists: `scripts/vault.sh exists <secret-name>` - List: `scripts/vault.sh ls` - Remove: `scripts/vault.sh rm <secret-name>` Naming policy: - Use stable scoped keys like `github/token`, `openai/prod/api_key`, `aws/staging/access_key_id`. Rotation policy: - Default to replacing value under the same key. - Use versioned keys only when user explicitly asks. ## Non-Negotiable Guardrails - Never ask users to paste raw secrets into chat. - Never echo secret values back to user. - Never store secrets in repo files, commit messages, issue comments, or transcripts. - Never expose intake over public interfaces or tunnels. - LAN mode must rely on runtime private-network autodetection and webform TOTP validation. ## Quick Runbook 1. Ensure TOTP enrollment exists (via setup preflight) before first LAN use. 2. For each missing secret, run intake in local or LAN mode based on user intent. 3. Execute tools via `run_with_secret.sh`. 4. Rotate/remove secrets via `vault.sh` as requested.

path: scripts/vault.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOF'
Usage:
  vault.sh put <secret-name>      # read secret from stdin and store
  vault.sh get <secret-name>      # print secret to stdout
  vault.sh ls                     # list secret names
  vault.sh exists <secret-name>   # exit 0 if exists
  vault.sh rm <secret-name>       # remove secret
EOF
}

require_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    echo "Missing required command: $1" >&2
    exit 1
  fi
}

validate_secret_name() {
  local value="$1"
  if [ -z "$value" ]; then
    echo "Secret name is required" >&2
    exit 1
  fi
  if [[ "$value" == -* ]]; then
    echo "Secret name must not start with '-'" >&2
    exit 1
  fi
  if [[ ! "$value" =~ ^[A-Za-z0-9._/-]+$ ]]; then
    echo "Secret name contains invalid characters" >&2
    exit 1
  fi
  if [[ "$value" == *".."* ]] || [[ "$value" == *"//"* ]]; then
    echo "Secret name contains invalid path traversal pattern" >&2
    exit 1
  fi
  if [[ "$value" == /* ]] || [[ "$value" == */ ]]; then
    echo "Secret name must not start or end with '/'" >&2
    exit 1
  fi
}

if [ "${1:-}" = "" ]; then
  usage
  exit 1
fi

cmd="$1"
name="${2:-}"

if [ "$cmd" = "-h" ] || [ "$cmd" = "--help" ]; then
  usage
  exit 0
fi

require_cmd pass

if [ "$cmd" != "ls" ] && [ -z "$name" ]; then
  usage
  exit 1
fi

if [ "$cmd" != "ls" ]; then
  validate_secret_name "$name"
fi

case "$cmd" in
  put)
    # Read exactly from stdin and write as multiline secret.
    pass insert -m -f -- "$name" >/dev/null
    ;;
  get)
    pass show -- "$name"
    ;;
  ls)
    pass ls
    ;;
  exists)
    pass show -- "$name" >/dev/null 2>&1
    ;;
  rm)
    pass rm -f -- "$name" >/dev/null
    ;;
  *)
    usage
    exit 1
    ;;
esac


path: scripts/setup_totp_enrollment.py
content: #!/usr/bin/env python3
import base64
from datetime import datetime, timezone
import json
import os
import secrets
import shutil
import subprocess
import sys
from pathlib import Path


def _install_hint() -> str:
    if sys.platform == "darwin":
        return "brew install qrencode"
    if shutil.which("apt-get"):
        return "sudo apt-get install -y qrencode"
    if shutil.which("dnf"):
        return "sudo dnf install -y qrencode"
    if shutil.which("pacman"):
        return "sudo pacman -S --needed qrencode"
    return "install qrencode using your system package manager"


def _base32_secret(num_bytes: int = 20) -> str:
    return base64.b32encode(secrets.token_bytes(num_bytes)).decode("ascii").rstrip("=")


def _decode_base32(value: str) -> bytes:
    cleaned = value.strip().replace(" ", "")
    padding = "=" * ((8 - len(cleaned) % 8) % 8)
    return base64.b32decode(cleaned.upper() + padding, casefold=True)


def main() -> int:
    base_dir = Path.home() / ".passwordstore-broker"
    secret_file = base_dir / "totp.secret"
    qr_png_path = base_dir / "totp-enroll.png"
    setup_timestamp_file = base_dir / "setup_completed_at.txt"

    base_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
    try:
        os.chmod(base_dir, 0o700)
    except OSError:
        pass

    provisioned = False
    if secret_file.exists():
        [REDACTED_SECRET]"utf-8").strip().replace(" ", "")
        if not [REDACTED_SECRET]"TOTP secret file is empty: {secret_file}", file=sys.stderr)
            return 2
    else:
        [REDACTED_SECRET])
        secret_file.write_text(secret + "\n", encoding="utf-8")
        provisioned = True

    try:
        _decode_base32(secret)
    except Exception:  # noqa: BLE001
        print(f"Invalid base32 value in TOTP secret file: {secret_file}", file=sys.stderr)
        return 2

    try:
        os.chmod(secret_file, 0o600)
    except OSError:
        pass

    if setup_timestamp_file.exists():
        setup_completed_at = setup_timestamp_file.read_text(encoding="utf-8").strip()
    else:
        setup_completed_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
        setup_timestamp_file.write_text(setup_completed_at + "\n", encoding="utf-8")
    try:
        os.chmod(setup_timestamp_file, 0o600)
    except OSError:
        pass

    otpauth_url = (
        "otpauth://totp/Passwordstore%20Broker"
        f"?[REDACTED_SECRET]"
    )

    payload = {
        "secret_file": str(secret_file),
        "otpauth_url": otpauth_url,
        "qr_png_path": str(qr_png_path),
        "setup_timestamp_file": str(setup_timestamp_file),
        "setup_completed_at": setup_completed_at,
        "provisioned": provisioned,
    }

    qrencode = shutil.which("qrencode")
    if qrencode is None:
        print(json.dumps(payload), flush=True)
        print(f"Missing required command: qrencode. Install with: {_install_hint()}", file=sys.stderr)
        print(f"Manual enrollment URL: {otpauth_url}", file=sys.stderr)
        return 2

    try:
        subprocess.run(
            [qrencode, "-o", str(qr_png_path), "-t", "PNG", otpauth_url],
            check=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            text=True,
        )
    except subprocess.CalledProcessError as exc:
        print(json.dumps(payload), flush=True)
        message = exc.stderr.strip() or "qrencode failed"
        print(f"Failed to generate QR PNG: {message}", file=sys.stderr)
        print(f"Manual enrollment URL: {otpauth_url}", file=sys.stderr)
        return 2

    try:
        os.chmod(qr_png_path, 0o600)
    except OSError:
        pass

    print(json.dumps(payload), flush=True)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/run_with_secret.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOF'
Usage:
  run_with_secret.sh --secret <name> --env <ENV_NAME> -- <command> [args...]

Example:
  run_with_secret.sh --secret github/token --env GITHUB_TOKEN -- gh api user
EOF
}

secret_name=""
env_name=""
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
vault_script="${script_dir}/vault.sh"

while [ $# -gt 0 ]; do
  case "$1" in
    -h|--help)
      usage
      exit 0
      ;;
    --secret)
      secret_name="${2:-}"
      shift 2
      ;;
    --env)
      env_name="${2:-}"
      shift 2
      ;;
    --)
      shift
      break
      ;;
    *)
      echo "Unknown arg: $1" >&2
      usage
      exit 1
      ;;
  esac
done

if [ -z "$secret_name" ] || [ -z "$env_name" ] || [ $# -eq 0 ]; then
  usage
  exit 1
fi

if [[ ! "$env_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
  echo "Invalid env var name: $env_name" >&2
  exit 1
fi

if [ ! -x "$vault_script" ]; then
  echo "Missing executable vault script: $vault_script" >&2
  exit 1
fi

if ! secret_value="$("$vault_script" get "$secret_name")"; then
  echo "Failed to read [REDACTED_SECRET]" >&2
  exit 1
fi

exec env "$env_name=$secret_value" "$@"


path: references/SETUP.md
content: # Installation and Initial Setup

This document is for one-time setup of `passwordstore-broker` protocol and tools.

## Official references

- https://www.passwordstore.org/
- https://www.passwordstore.org/#setting-it-up

## Required tools

- `pass`
- `gpg`
- `openssl`
- `python3`
- `qrencode` (required for PNG QR enrollment output)

## Install commands

### macOS (Homebrew)

```bash
brew install pass gnupg openssl python qrencode
```

### Debian/Ubuntu

```bash
sudo apt-get update
sudo apt-get install -y pass gnupg2 openssl python3 qrencode
```

### Fedora/RHEL

```bash
sudo dnf install -y pass gnupg2 openssl python3 qrencode
```

### Arch Linux

```bash
sudo pacman -S --needed pass gnupg openssl python qrencode
```

## Initialize password store

1. Create a GPG key (if needed):

```bash
gpg --full-generate-key
```

2. Find key id/email:

```bash
gpg --list-secret-keys --keyid-format LONG
```

3. Initialize `pass`:

```bash
pass init "<your-gpg-key-id-or-email>"
```

4. Optional upstream recommendation:

```bash
pass git init
```

Notes:
- `pass init` accepts multiple recipients.
- Per-subfolder recipients are supported via `pass init -p <subfolder> <gpg-id>`.

## Verify base installation

```bash
pass --version
gpg --version
openssl version
python3 --version
qrencode --version
```

## Agent-driven TOTP enrollment setup

Run once on the machine where `passwordstore-broker` executes:

```bash
scripts/setup_totp_enrollment.py
```

Expected JSON output:

- `secret_file`: default `~/.passwordstore-broker/totp.secret`
- `otpauth_url`: fallback manual authenticator enrollment URL
- `qr_png_path`: default `~/.passwordstore-broker/totp-enroll.png`
- `setup_timestamp_file`: default `~/.passwordstore-broker/setup_completed_at.txt`
- `setup_completed_at`: initial setup timestamp (UTC ISO-8601)
- `provisioned`: `true` if secret was created, `false` if reused

Expected permissions/artifacts:

- `~/.passwordstore-broker` mode `0700`
- `~/.passwordstore-broker/totp.secret` mode `0600`
- `~/.passwordstore-broker/totp-enroll.png` mode `0600` (best effort)
- `~/.passwordstore-broker/setup_completed_at.txt` mode `0600`

Security rule:
- The TOTP secret is enrollment-only material. Do not share or retransmit `totp.secret` contents after initial setup.
- For the initial setup send the QR code image at `qr_png_path` (preferred) or the `otpauth_url` to the user for enrollment in their authenticator app.

## Failure handling

- Invalid/expired TOTP code: submission fails with authentication error.
- Too many failed TOTP attempts: temporary lockout is applied.
- Lockout policy: 5 failed attempts within 5 minutes causes 5-minute lockout.
- After lockout expires, user can retry with a fresh TOTP code.
- If `qrencode` is missing during enrollment, script exits non-zero and prints install guidance while still returning `otpauth_url` fallback.

## Sanity check

```bash
scripts/vault.sh put test/secret <<< "ok"
scripts/vault.sh exists test/secret
scripts/run_with_secret.sh --secret test/secret --env TEST_SECRET -- sh -c 'test "$TEST_SECRET" = "ok"'
scripts/vault.sh rm test/secret
```


Security scan verdicts — ClawScan: clean | static analysis:  | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)