#!/bin/zsh # Paste a password from 1Password into a tmux pane, e.g. at a remote sudo # prompt. Run in a popup (see .mac_config): # op_password_paste.zsh [client_name] emulate -L zsh setopt extended_glob pipe_fail no_unset pane=${1:?usage: op_password_paste.zsh [client_name]} client=${2:-} # -l: messages may contain item titles, which must not be expanded as formats die() { print -ru2 -- "1Password: $*" tmux display-message -l ${client:+-c} $client "1Password: $*" # keep the popup (and op's error output) visible until a key is pressed [[ -t 0 ]] && read -sk1 "?Press any key to close." exit 1 } (( $+commands[op] )) || die "op CLI missing (brew install --cask 1password-cli)" # Guard: a paste outside a password prompt echoes the secret (and may run it). # The cursor line must end in a prompt like "[sudo] password for x:". cursor_line() { local pos=$(tmux display-message -p -t $pane '#{cursor_x},#{cursor_y}') print -r -- "$pos $(tmux capture-pane -p -t $pane -S ${pos#*,} -E ${pos#*,})" } prompt=$(cursor_line) if [[ ${prompt#* } != *(#i)(password|passphrase|passwort|kennwort)*:[[:space:]]# ]]; then print -r -- "No password prompt at the cursor:" print -r -- " ${${prompt#* }:-}" read -q "?Paste anyway? [y/N] " || exit 0 print fi # Destination of an ssh running in the pane, used as the initial fzf query. ssh_host() { local -a words=(${(z)1}) local w i [[ ${words[1]:t} == ssh ]] || return 1 for (( i = 2; i <= $#words; i++ )); do w=${words[i]} if [[ $w == -- ]]; then w=${words[i+1]:-} break elif [[ $w == -?* ]]; then # option letters taking an argument; the last letter may take the next word [[ $w == -[^BbcDEeFIiJLlmOoPpQRSWw]#[BbcDEeFIiJLlmOoPpQRSWw] ]] && (( i++ )) else break fi done w=${${${w#ssh://}#*@}%%/*} w=${w%:[0-9]##} [[ -n $w ]] && print -r -- $w } host= tty=$(tmux display-message -p -t $pane '#{pane_tty}') for args in ${(f)"$(ps -o args= -t ${tty#/dev/} 2>/dev/null)"}; do host=$(ssh_host $args) && break done # All signed-in accounts; the desktop app integration asks for Touch ID. accounts=(${(f)"$(op account list --format json | jq -r '.[].account_uuid')"}) || die "op account list failed" (( $#accounts )) || die "no 1Password account (enable CLI integration in the app)" rows=$(for acc in $accounts; do op item list --account $acc --categories Login,Password,Server --format json | jq -r --arg acc $acc '.[] | [$acc, .vault.id, .id, .title, (.additional_information // ""), .vault.name, ([.urls[]?.href] | join(" "))] | @tsv' || exit 1 done) || die "op item list failed for an account" [[ -n $rows ]] || die "no Login/Password/Server items found" # The prefill comes from the local ssh; after another hop it names the wrong host. choice=$(print -r -- $rows | fzf --delimiter='\t' --with-nth=4.. --reverse \ --query=$host --prompt='password> ' \ --header="paste into $pane${host:+ (local ssh: $host)}") || exit 0 fields=("${(@ps:\t:)choice}") pw=$(op read --account $fields[1] "op://$fields[2]/$fields[3]/password") || die "no 'password' field in ${fields[4]}" # Named buffer, deleted by paste-buffer -d; no clipboard (no load-buffer -w). buf=op-password-$$ trap 'tmux delete-buffer -b $buf 2>/dev/null' EXIT print -rn -- $pw | tmux load-buffer -b $buf - pw= # Picking and Touch ID take a while: refuse if the prompt moved meanwhile # (sudo timeout, dropped connection). [[ "$(cursor_line)" == "$prompt" ]] || die "pane changed since the check, not pasted" tmux paste-buffer -d -b $buf -t $pane