This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / tools / pinentry-rofi / pinentry-rofi.sh
7.8 kB 285 lines
1#!/usr/bin/env bash 2# pinentry-rofi: A rofi-based pinentry 3# 4# Dual-mode: 5# - SSH_ASKPASS mode: invoked with a prompt string as $1 (e.g. by 6# ssh-tpm-agent via SSH_ASKPASS). Prints the PIN on stdout. 7# - Assuan/pinentry mode: invoked with no args or only "--" options 8# (e.g. by rbw, gpg-agent). Speaks the Assuan protocol on stdin/stdout. 9# 10# Caches PIN in kernel keyring for 5 minutes (sliding window). 11set -efu 12 13CACHE_KEY="pinentry-rofi-pin" 14CACHE_TIMEOUT=300 # 5 minutes 15 16# Hard limit on how long a dialog may sit unanswered. Without it, a prompt 17# nobody answers (lost workspace, requester already dead) hangs forever, and 18# ssh-tpm-agent serializes TPM access behind it: every later signature blocks, 19# so all ssh auth on the machine wedges until the stray rofi is killed by hand. 20# On expiry `timeout` exits 124, which the callers treat like a cancel, so the 21# requester gets a failed auth it can retry instead of a deadlock. 22PROMPT_TIMEOUT="${PINENTRY_ROFI_TIMEOUT:-120}" 23 24# ---------- cache helpers ---------- 25 26get_cached_pin() { 27 local key_id 28 key_id=$(keyctl search @u user "$CACHE_KEY" 2>/dev/null) || return 1 29 keyctl pipe "$key_id" 2>/dev/null 30} 31 32cache_pin() { 33 local pin="$1" 34 keyctl purge user "$CACHE_KEY" >/dev/null 2>&1 || true 35 local key_id 36 key_id=$(printf '%s' "$pin" | keyctl padd user "$CACHE_KEY" @u) || return 0 37 keyctl timeout "$key_id" "$CACHE_TIMEOUT" || true 38} 39 40extend_cache() { 41 local key_id 42 key_id=$(keyctl search @u user "$CACHE_KEY" 2>/dev/null) || return 0 43 keyctl timeout "$key_id" "$CACHE_TIMEOUT" || true 44} 45 46clear_cache() { 47 keyctl purge user "$CACHE_KEY" >/dev/null 2>&1 || true 48} 49 50# ---------- SSH context (for ssh-tpm-agent) ---------- 51 52get_ssh_args() { 53 local pid="${SSH_CLIENT_PID:-}" 54 [[ -z "$pid" ]] && return 1 55 [[ ! -r "/proc/$pid/cmdline" ]] && return 1 56 57 local -a args=() 58 while IFS= read -r -d '' arg; do 59 args+=("$arg") 60 done < "/proc/$pid/cmdline" 61 62 [[ "${args[0]}" == "ssh" || "${args[0]}" == */ssh ]] || return 1 63 64 local i=1 65 local line="" 66 while [[ $i -lt ${#args[@]} ]]; do 67 local arg="${args[$i]}" 68 if [[ "$arg" == -* ]]; then 69 [[ -n "$line" ]] && echo "$line" 70 line="$arg" 71 else 72 if [[ -n "$line" ]]; then 73 line="$line $arg" 74 else 75 line="$arg" 76 fi 77 fi 78 ((i++)) 79 done 80 [[ -n "$line" ]] && echo "$line" 81} 82 83escape_pango() { 84 sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' 85} 86 87# ---------- rofi prompt ---------- 88# 89# Args: $1 = prompt label (default "PIN") 90# $2 = optional pango-formatted -mesg text 91# $3 = use_cache: "cache" enables kernel-keyring caching, anything 92# else disables it (Assuan callers like rbw/gpg-agent do their 93# own caching, and sharing one keyring slot across unrelated 94# secrets is wrong). 95# Echoes PIN on success, returns non-zero on cancel. 96rofi_prompt() { 97 local prompt_label="${1:-PIN}" 98 local mesg="${2:-}" 99 local use_cache="${3:-}" 100 101 local mesg_args=() 102 [[ -n "$mesg" ]] && mesg_args=(-mesg "$mesg") 103 104 local theme_file="/var/theme/config/rofi-theme" 105 local theme_args=() 106 if [[ -r "$theme_file" ]]; then 107 local theme 108 theme=$(cat "$theme_file") 109 theme_args=(-theme "$theme") 110 fi 111 112 # Cached PIN menu (only in modes that opted in) 113 if [[ "$use_cache" == "cache" ]]; then 114 local cached_pin="" 115 if cached_pin=$(get_cached_pin 2>/dev/null) && [[ -n "$cached_pin" ]]; then 116 local choice 117 choice=$(printf "OK\nCancel\nClear cache" | timeout -k 5 "$PROMPT_TIMEOUT" rofi -dmenu \ 118 -i \ 119 -no-custom \ 120 -no-fixed-num-lines \ 121 "${mesg_args[@]}" \ 122 "${theme_args[@]}" \ 123 2>/dev/null) || return 1 124 125 case "$choice" in 126 "OK") 127 extend_cache 128 printf '%s' "$cached_pin" 129 return 0 130 ;; 131 "Cancel") 132 return 1 133 ;; 134 "Clear cache") 135 clear_cache 136 ;; 137 esac 138 fi 139 fi 140 141 local result 142 result=$(timeout -k 5 "$PROMPT_TIMEOUT" rofi -dmenu \ 143 -password \ 144 -p "$prompt_label" \ 145 -lines 0 \ 146 -no-fixed-num-lines \ 147 "${mesg_args[@]}" \ 148 "${theme_args[@]}" \ 149 < /dev/null 2>/dev/null) || return 1 150 151 [[ -z "$result" ]] && return 1 152 153 if [[ "$use_cache" == "cache" ]]; then 154 cache_pin "$result" 155 fi 156 printf '%s' "$result" 157} 158 159# ---------- mode: SSH_ASKPASS ---------- 160 161askpass_main() { 162 local ssh_args="" 163 ssh_args=$(get_ssh_args 2>/dev/null | escape_pango) || ssh_args="" 164 165 local mesg="" 166 if [[ -n "$ssh_args" ]]; then 167 mesg="<b>SSH:</b> 168$ssh_args" 169 fi 170 171 local pin 172 pin=$(rofi_prompt "PIN" "$mesg" cache) || exit 1 173 printf '%s\n' "$pin" 174} 175 176# ---------- mode: Assuan pinentry ---------- 177# 178# Spec (abridged): https://www.gnupg.org/documentation/manuals/assuan/ 179# Server greets with "OK ...", responds OK to most settings, and on 180# GETPIN replies with "D <pin>" then "OK", or "ERR 83886179 cancelled". 181# Data lines must percent-encode %, CR, LF. 182 183assuan_decode() { 184 # decode %HH escapes from SETDESC/SETPROMPT/SETERROR values 185 printf '%b' "$(printf '%s' "$1" | sed 's/%\([0-9A-Fa-f][0-9A-Fa-f]\)/\\x\1/g')" 186} 187 188assuan_encode_data() { 189 # encode %, CR, LF for D-line output 190 printf '%s' "$1" | sed -e 's/%/%25/g' -e 's/\r/%0D/g' 191} 192 193assuan_main() { 194 printf 'OK Pleased to meet you\n' 195 196 local desc="" prompt="PIN" error="" 197 # title and keyinfo are accepted but unused by the rofi UI 198 local _title="" _keyinfo="" 199 200 local line cmd arg 201 while IFS= read -r line; do 202 line="${line%$'\r'}" 203 if [[ "$line" == *" "* ]]; then 204 cmd="${line%% *}" 205 arg="${line#* }" 206 else 207 cmd="$line" 208 arg="" 209 fi 210 211 case "$cmd" in 212 SETDESC) desc="$(assuan_decode "$arg")"; printf 'OK\n' ;; 213 SETPROMPT) prompt="$(assuan_decode "$arg")"; printf 'OK\n' ;; 214 SETERROR) error="$(assuan_decode "$arg")"; printf 'OK\n' ;; 215 SETTITLE) _title="$(assuan_decode "$arg")"; printf 'OK\n' ;; 216 SETKEYINFO) _keyinfo="$arg"; printf 'OK\n' ;; 217 218 OPTION|SETOK|SETCANCEL|SETNOTOK|SETTIMEOUT|SETQUALITYBAR|SETQUALITYBAR_TT|SETREPEAT|SETREPEATERROR|SETGENPIN|SETGENPIN_TT|CLEARPASSPHRASE|GETINFO) 219 printf 'OK\n' ;; 220 221 GETPIN) 222 # Build rofi -mesg from desc/error 223 local mesg="" 224 if [[ -n "$error" ]]; then 225 mesg="<b>$(printf '%s' "$error" | escape_pango)</b>" 226 error="" 227 fi 228 if [[ -n "$desc" ]]; then 229 [[ -n "$mesg" ]] && mesg+=" 230" 231 mesg+="$(printf '%s' "$desc" | escape_pango)" 232 fi 233 234 local pin 235 # No cache in Assuan mode: each caller (rbw, gpg-agent, ...) 236 # asks for a different secret and does its own caching. 237 if pin=$(rofi_prompt "${prompt:-PIN}" "$mesg" no-cache); then 238 printf 'D %s\n' "$(assuan_encode_data "$pin")" 239 printf 'OK\n' 240 else 241 printf 'ERR 83886179 Operation cancelled\n' 242 fi 243 ;; 244 245 CONFIRM|MESSAGE) 246 printf 'OK\n' ;; 247 248 RESET) 249 desc=""; prompt="PIN"; error=""; _title=""; _keyinfo="" 250 printf 'OK\n' ;; 251 252 NOP) 253 printf 'OK\n' ;; 254 255 BYE) 256 printf 'OK closing connection\n' 257 exit 0 ;; 258 259 "") 260 ;; 261 262 *) 263 printf 'OK\n' ;; 264 esac 265 done 266} 267 268# ---------- dispatch ---------- 269 270is_assuan_mode() { 271 # Assuan/pinentry mode: no args, or first arg is a flag. 272 # Real pinentry callers (rbw, gpg-agent) always lead with flags like 273 # --timeout 0 --ttyname /dev/pts/0 --display :0 --no-global-grab 274 # Note we can't require *every* arg to start with '-', because flag 275 # values ("0", "/dev/pts/0", ":0", ...) don't. 276 # 277 # SSH_ASKPASS mode: first arg is a bare prompt string (no leading '-'). 278 [[ $# -eq 0 || "${1:-}" == -* ]] 279} 280 281if is_assuan_mode "$@"; then 282 assuan_main 283else 284 askpass_main 285fi