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