diff --git a/config/dot/quickshell/scripts/panama-pick b/config/dot/quickshell/scripts/panama-pick index 073fff4..c6ae958 100755 --- a/config/dot/quickshell/scripts/panama-pick +++ b/config/dot/quickshell/scripts/panama-pick @@ -7,6 +7,7 @@ # panama-pick process pick a process by CPU; SIGTERM it # panama-pick ssh pick a Host from ~/.ssh/config; open a session # panama-pick recent pick a recently used file; open it +# panama-pick password pick a Bitwarden entry; copy it, concealed # # One helper rather than five scripts because every subcommand is the same # sentence: build a list, let dmenu pick a line, act on the index. dmenu is @@ -96,8 +97,46 @@ PY [[ -n "$idx" ]] || exit 0 exec xdg-open "$(line_at "$idx" <<<"$entries")" ;; + password) + # The security shape, in one place: + # * the secret travels rbw -> pipe -> wl-copy; it is never an argument + # and never a file (argv is world-readable, files outlive intentions) + # * --sensitive offers the x-kde-passwordManagerHint MIME, which + # vicinae's clipboard history documents it ignores -- verified against + # the live history before this was written + # * the clipboard clears itself after 30 seconds, from a transient timer + # so it happens even if this shell is long gone + if ! command -v rbw >/dev/null 2>&1; then + notify-send "Passwords" "rbw is not installed" 2>/dev/null || true + exit 1 + fi + if ! entries="$(rbw list --fields name,user 2>/dev/null)" || [[ -z "$entries" ]]; then + notify-send "Passwords" \ + "rbw is not set up. Run: rbw config set email ; rbw register; rbw login" \ + 2>/dev/null || true + exit 0 + fi + idx="$(awk -F'\t' '{ if ($2 != "") printf "%s (%s)\n", $1, $2; else print $1 }' <<<"$entries" \ + | menu "Copy password" 'Bitwarden ({count})')" || exit 0 + [[ -n "$idx" ]] || exit 0 + picked="$(line_at "$idx" <<<"$entries")" + name="$(cut -f1 <<<"$picked")" + user="$(cut -f2 <<<"$picked")" + # `--` keeps an entry named like a flag from parsing as one; the pipeline + # status catches a vault that locked between list and get, so a failed + # read is reported instead of an empty copy claiming success. + if ! rbw get -- "$name" ${user:+"$user"} | wl-copy --trim-newline --sensitive; then + notify-send "Passwords" "Could not read \"$name\" — is the vault locked?" 2>/dev/null || true + exit 1 + fi + systemd-run --user --collect --on-active=30s \ + --unit="panama-clip-clear-$(date +%s)-$RANDOM" \ + wl-copy --clear >/dev/null 2>&1 || true + notify-send --icon=dialog-password-symbolic \ + "Password copied" "\"$name\" — the clipboard clears in 30 seconds" 2>/dev/null || true + ;; *) - echo 'usage: panama-pick window|quit-window|process|ssh|recent' >&2 + echo 'usage: panama-pick window|quit-window|process|ssh|recent|password' >&2 exit 2 ;; esac diff --git a/config/local/share/vicinae/scripts/copy-password b/config/local/share/vicinae/scripts/copy-password new file mode 100755 index 0000000..173da6e --- /dev/null +++ b/config/local/share/vicinae/scripts/copy-password @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# @vicinae.schemaVersion 1 +# @vicinae.title Copy Password +# @vicinae.mode silent +# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg +# @vicinae.description Pick a Bitwarden entry; the password lands on the clipboard, concealed from history, and clears in 30 seconds. +# @vicinae.keywords ["password", "bitwarden", "vault", "credentials", "login", "secret"] + +exec "$HOME/.config/quickshell/scripts/panama-pick" password diff --git a/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md b/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md index 7572c6e..5c6976e 100644 --- a/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md +++ b/docs/superpowers/plans/2026-08-21-vicinae-os-parity.md @@ -127,16 +127,17 @@ The list-picking half. One bash helper, not a compiled extension: `vicinae dmenu ### Task 8: Bitwarden via rbw — alone, last, carefully **Files:** -- Modify: `setup/packages/` (add `rbw`) -- Modify: `config/local/share/vicinae/extensions/panama-desktop/` (src/passwords.tsx) -- Create: `tests/setup/rbw-command-contract` +- Modify: `setup/packages/desktop-packages` (add `rbw`) +- Modify: `config/dot/quickshell/scripts/panama-pick` (password subcommand) +- Create: `config/local/share/vicinae/scripts/copy-password` +- Modify: `tests/setup/launcher-commands-contract` (password section) **Steps:** -- [ ] `rbw` added to a package list; first-run setup documented (`rbw config set email`, `rbw login`) and surfaced as a friendly message when unconfigured -- [ ] List entries from `rbw list`; copy password on select via `rbw get` piped to `wl-copy`, never argv, never a temp file -- [ ] Clipboard clears after 30 seconds (`wl-copy --clear` scheduled with `systemd-run --user --on-active=30s` or vicinae's own clipboard hygiene if it offers one — check first) -- [ ] Contract with a stub rbw: copy path never leaks the secret into argv or the environment of anything but wl-copy; unconfigured rbw degrades to the setup message -- [ ] Review pass on this task's diff before it merges, separate from writing it +- [x] `rbw` added to a package list; first-run setup documented (`rbw config set email`, `rbw login`) and surfaced as a friendly message when unconfigured +- [x] List entries from `rbw list`; copy password on select via `rbw get` piped to `wl-copy`, never argv, never a temp file +- [x] Clipboard clears after 30 seconds (`wl-copy --clear` scheduled with `systemd-run --user --on-active=30s` or vicinae's own clipboard hygiene if it offers one — check first) +- [x] Contract with a stub rbw: copy path never leaks the secret into argv or the environment of anything but wl-copy; unconfigured rbw degrades to the setup message +- [x] Review pass on this task's diff before it merges, separate from writing it ### Task 9: Documentation and closeout @@ -146,6 +147,8 @@ The list-picking half. One bash helper, not a compiled extension: `vicinae dmenu - Modify: this file — Findings section, boxes ticked **Steps:** -- [ ] README's Vicinae row reflects the new command families -- [ ] Every new file carries the why-comment the repo expects; no `.sh` extensions anywhere -- [ ] Full `panama test` run is green; README count matches +- [x] README's Vicinae row reflects the new command families +- [x] Every new file carries the why-comment the repo expects; no `.sh` extensions anywhere +- [x] Full `panama test` run is green; README count matches + +**Task 8 findings (2026-08-21):** `wl-copy --sensitive` (wl-clipboard 2.2) offers the password-manager hint MIME, and vicinae's clipboard docs commit to ignoring it — verified live against the real history database (a plain probe was recorded, a sensitive one was not) before the command was written. The clipboard clear is a 30s transient timer. Review pass caught and fixed a silent empty copy on a locked vault and a flag-shaped entry name. diff --git a/setup/packages/desktop-packages b/setup/packages/desktop-packages index acf23d2..cc0419c 100644 --- a/setup/packages/desktop-packages +++ b/setup/packages/desktop-packages @@ -72,6 +72,9 @@ openssl-devel opus-devel papers python3-dnf-plugin-versionlock +# The launcher's Copy Password command; talks to the same Bitwarden account +# the desktop app signs into. See panama-pick. +rbw rocm-opencl # The Snapshots page is snapper end to end; see scripts/panama-snapshots. snapper diff --git a/tests/setup/launcher-commands-contract b/tests/setup/launcher-commands-contract index 76aeeb0..f5da307 100755 --- a/tests/setup/launcher-commands-contract +++ b/tests/setup/launcher-commands-contract @@ -38,7 +38,7 @@ mkdir -p "$stub_dir" # ── 1. Vicinae accepts every new command ───────────────────────────────────── commands=(lock-screen suspend-system log-out reboot-system power-off - remind-me list-reminders pick-color + remind-me list-reminders pick-color copy-password switch-window force-quit-window kill-process ssh-hosts recent-files) for command in "${commands[@]}"; do path="$scripts/$command" @@ -195,6 +195,64 @@ DMENU_ANSWER=1 HOME="$fake_home" PATH="$stub_dir:$PATH" "$pick" recent >/dev/nul grep -q 'xdg-open /tmp/older file.txt' "$calls" \ || note 'the picked recent file was not opened with its URI decoded (newest-first order, %20 as space)' +# ── 6. Passwords: the secret's whole journey is a pipe ─────────────────────── +# +# The rules that make Copy Password safe to ship at all: +# * the secret is never an argument to anything (argv is world-readable) +# * it reaches wl-copy with --sensitive, the hint vicinae's clipboard +# history documents it ignores -- without it, passwords land in a +# plaintext FTS database +# * a clipboard clear is scheduled the moment the copy happens +# * an unconfigured rbw degrades to a setup message, never an error + +secret='s3cr3t-fixture-value' +cat >"$stub_dir/rbw" <>"$calls" +case "\$1" in + list) printf 'github\tgib\nrouter\t\n';; + get) printf '%s\n' '$secret';; +esac +STUB +chmod +x "$stub_dir/rbw" +cat >"$stub_dir/wl-copy" <>"$calls" +cat >"$work/wl-copy-stdin" +STUB +chmod +x "$stub_dir/wl-copy" + +: >"$calls"; : >"$work/wl-copy-stdin" +DMENU_ANSWER=0 PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 \ + || note 'copying the first password entry failed' +grep -q "$secret" "$calls" \ + && note 'the secret appears on a command line' +grep -qx "$secret" "$work/wl-copy-stdin" \ + || note 'the secret does not reach wl-copy on stdin' +grep -q 'wl-copy.*--sensitive' "$calls" \ + || note 'the copy is not marked sensitive, so it lands in clipboard history' +grep -q 'rbw get -- github gib' "$calls" \ + || note 'the picked entry name and user do not reach rbw get, flag-safe' +grep -qE 'systemd-run .*--on-active=30s .*wl-copy --clear' "$calls" \ + || note 'no clipboard clear is scheduled after the copy' + +# Escape copies nothing. +: >"$calls" +PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 +grep -q 'wl-copy' "$calls" && note 'Escape from the password list still copied something' + +# Unconfigured rbw is a setup hint, not a failure. +cat >"$stub_dir/rbw" <<'STUB' +#!/usr/bin/env bash +exit 1 +STUB +chmod +x "$stub_dir/rbw" +: >"$calls" +PATH="$stub_dir:$PATH" "$pick" password >/dev/null 2>&1 \ + || note 'an unconfigured rbw is treated as a crash instead of a setup hint' +grep -q 'not set up' "$calls" \ + || note 'an unconfigured rbw does not explain how to set it up' + if (( ${#findings[@]} > 0 )); then printf 'launcher commands contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2