93 lines
3.6 KiB
Bash
Executable File
93 lines
3.6 KiB
Bash
Executable File
#!/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 <pane_id> [client_name]
|
|
emulate -L zsh
|
|
setopt extended_glob pipe_fail no_unset
|
|
|
|
pane=${1:?usage: op_password_paste.zsh <pane_id> [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#* }:-<empty line>}"
|
|
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
|