Dotfiles/desktopenvs/hyprland/scripts/amssh

660 lines
25 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env bash
# amssh — encrypted SSH login manager
# Storage : ~/.amssh (AES-256-CBC · PBKDF2-SHA256 · 600 000 iterations)
# Modes : --tui (default, fzf TUI) | --drun (wofi launcher mode)
# Auth : master passphrase; FIDO2/PAM layer if pam_u2f + pamtester present
set -euo pipefail
# ── constants ────────────────────────────────────────────────────────────────
STORE="${AMSSH_STORE:-$HOME/.amssh}"
CONF_DIR="${HOME}/.config/amssh"
AUTH_CONF="${CONF_DIR}/auth"
ITERS=600000
TERM_CMD="${AMSSH_TERM:-kitty}"
MODE="tui"
VERSION="#amssh:v1"
_TMP=$(mktemp -d /tmp/amssh.XXXXXX)
trap 'rm -rf "$_TMP"' EXIT
# ── usage ────────────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
amssh — encrypted SSH login manager
Usage:
amssh [--tui] Interactive fzf TUI (default)
amssh --drun Wofi launcher mode
TUI keys:
Enter Connect to selected host
Ctrl-A Add new entry
Ctrl-E Edit selected entry
Ctrl-D Delete selected entry
Esc Quit
Store: ~/.amssh (AES-256-CBC, PBKDF2-SHA256)
EOF
}
# ── argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--tui|-t) MODE="tui" ;;
--drun|-d) MODE="drun" ;;
--help|-h) _usage; exit 0 ;;
*) printf '[amssh] Unknown option: %s\n' "$1" >&2; _usage; exit 1 ;;
esac
shift
done
# ── crypto ───────────────────────────────────────────────────────────────────
_decrypt() {
AMSSH_PASS="$1" openssl enc -aes-256-cbc -pbkdf2 -iter "$ITERS" -d \
-in "$STORE" -pass env:AMSSH_PASS 2>/dev/null
}
_encrypt_stdin() {
AMSSH_PASS="$1" openssl enc -aes-256-cbc -pbkdf2 -iter "$ITERS" \
-out "$STORE" -pass env:AMSSH_PASS
}
# ── passphrase helpers ────────────────────────────────────────────────────────
_ask_tty() {
local p
printf '%s' "${1:-Passphrase: }" >&2
IFS= read -rs p </dev/tty
printf '\n' >&2
printf '%s' "$p"
}
_ask_pinentry() {
local cmd
cmd=$(command -v pinentry-qt 2>/dev/null \
|| command -v pinentry-gtk-2 2>/dev/null \
|| command -v pinentry 2>/dev/null) || return 1
local out
out=$(printf 'SETPROMPT amssh master passphrase\nSETDESC Enter master passphrase for the encrypted SSH store\nGETPIN\n' \
| "$cmd" 2>/dev/null) || return 1
printf '%s\n' "$out" | grep -q '^ERR' && return 1
printf '%s\n' "$out" | awk '/^D /{sub(/^D /,""); print; exit}'
}
# ── FIDO2 / PAM detection ────────────────────────────────────────────────────
PAM_SVC=amssh
PAM_SVC_FILE="/etc/pam.d/${PAM_SVC}"
_find_pam_u2f() {
local f
for f in /lib/security/pam_u2f.so \
/usr/lib/security/pam_u2f.so \
/lib/x86_64-linux-gnu/security/pam_u2f.so \
/usr/lib/x86_64-linux-gnu/security/pam_u2f.so \
/usr/lib/aarch64-linux-gnu/security/pam_u2f.so; do
[[ -f "$f" ]] && printf '%s' "$f" && return 0
done
return 1
}
_fido_keys_file() {
local f
for f in "$HOME/.config/Yubico/u2f_keys" "$HOME/.config/pam-u2f/keys"; do
[[ -f "$f" ]] && printf '%s' "$f" && return 0
done
return 1
}
# Hardware + tool present — enough to offer FIDO setup in the dialog.
_fido_hardware_available() {
_find_pam_u2f >/dev/null && command -v pamtester &>/dev/null
}
# Fully configured — all four prerequisites ready to authenticate.
_fido_pam_available() {
_fido_hardware_available \
&& _fido_keys_file >/dev/null \
&& [[ -f "$PAM_SVC_FILE" ]]
}
# Creates /etc/pam.d/amssh — requires sudo once.
_ensure_pam_service() {
[[ -f "$PAM_SVC_FILE" ]] && return 0
printf '[amssh] Creating PAM service %s (requires sudo)\n' "$PAM_SVC_FILE" >&2
sudo tee "$PAM_SVC_FILE" >/dev/null <<'PAM'
#%PAM-1.0
auth required pam_u2f.so cue
PAM
}
# Registers a FIDO key into ~/.config/Yubico/u2f_keys if not already present.
_register_fido_key() {
local keys_file="$HOME/.config/Yubico/u2f_keys"
if [[ -f "$keys_file" ]]; then
printf '[amssh] Key file already exists at %s\n' "$keys_file" >&2
printf '[amssh] To add another key run: pamu2fcfg -n >> %s\n' "$keys_file" >&2
return 0
fi
mkdir -p "$(dirname "$keys_file")"
printf '[amssh] Insert your FIDO key, then press Enter...\n' >&2
read -r -s < /dev/tty
printf '[amssh] Touch your FIDO key when it blinks...\n' >&2
if pamu2fcfg > "$keys_file" 2>/dev/tty; then
printf '[amssh] Key registered at %s\n' "$keys_file" >&2
else
rm -f "$keys_file"
printf '[amssh] Registration failed — check key and retry\n' >&2
return 1
fi
}
_pam_authenticate() {
# Both stdout and stderr must go to /dev/tty: pamtester prints its success
# message to stdout, which would contaminate pass=$(_get_passphrase) if left
# uncaptured. stderr carries the pam_u2f tap prompt.
pamtester "$PAM_SVC" "$USER" authenticate >/dev/tty 2>&1
}
# ── master auth (passphrase + optional FIDO2 PAM layer) ─────────────────────
_get_passphrase() {
# FIDO2/PAM second-factor: opt-in via AMSSH_PAM=1
if [[ "${AMSSH_PAM:-}" == "1" ]] && _fido_pam_available; then
printf '[amssh] FIDO2/PAM authentication required\n' >&2
_pam_authenticate || { printf '[amssh] PAM auth failed\n' >&2; exit 1; }
printf '[amssh] PAM OK\n' >&2
fi
local pass
if [[ "$MODE" == "drun" ]]; then
pass=$(_ask_pinentry) || pass=$(_ask_tty "amssh passphrase: ")
else
pass=$(_ask_tty "amssh passphrase: ")
fi
[[ -z "$pass" ]] && { printf '[amssh] No passphrase provided\n' >&2; exit 1; }
printf '%s' "$pass"
}
# ── store helpers ─────────────────────────────────────────────────────────────
# Entry format: alias|user|host|port|identity|description
# (port defaults to 22; identity and description may be empty)
_init_store() {
printf '%s\n' "$VERSION" | _encrypt_stdin "$1"
}
_verify_pass() {
_decrypt "$1" 2>/dev/null | grep -q "^${VERSION}" || return 1
}
_load_entries() {
_decrypt "$1" | grep -v '^#' | grep -v '^[[:space:]]*$' || true
}
_save_entries() {
# reads entry lines from stdin, prepends version header, encrypts
local pass="$1"
{ printf '%s\n' "$VERSION"; cat; } | grep -v '^[[:space:]]*$' | _encrypt_stdin "$pass"
}
# ── display formatting ────────────────────────────────────────────────────────
# Entry format: alias|user|host|port|identity|description|password (7 fields)
# password is stored in plaintext inside the AES-256 encrypted store.
_entry_to_display() {
awk -F'|' '{
addr = $2 "@" $3
if ($4 != "" && $4 != "22") addr = addr ":" $4
line = $1 " \342\200\224 " addr
if ($7 != "") line = line " [*]"
if ($6 != "") line = line " \342\200\224 " $6
print line
}'
}
# ── SSH connector ─────────────────────────────────────────────────────────────
_connect() {
local entry="$1"
local alias user host port identity desc password
IFS='|' read -r alias user host port identity desc password <<< "$entry"
local args=()
[[ -n "$port" && "$port" != "22" ]] && args+=(-p "$port")
[[ -n "$identity" ]] && args+=(-i "$identity")
args+=("${user}@${host}")
printf '[amssh] Connecting: ssh %s\n' "${args[*]}" >&2
if [[ -n "$password" ]]; then
# Use SSH_ASKPASS_REQUIRE=force so SSH calls our script for the password
# instead of prompting interactively — works with OpenSSH 8.4+
local askpass="$_TMP/askpass"
printf '#!/bin/sh\nprintf "%%s" "$AMSSH_CONN_PW"\n' > "$askpass"
chmod 700 "$askpass"
AMSSH_CONN_PW="$password" SSH_ASKPASS="$askpass" \
SSH_ASKPASS_REQUIRE=force ssh "${args[@]}"
else
ssh "${args[@]}"
fi
}
# ── prompt for entry (drun / zenity) ─────────────────────────────────────────
_prompt_entry_zenity() {
local result
result=$(zenity --forms \
--title="amssh — Add Entry" \
--text="SSH login details" \
--add-entry="user@host[:port]" \
--add-entry="Alias" \
--add-password="SSH password (optional)" \
--separator='|' 2>/dev/null) || return 1
local target alias password
IFS='|' read -r target alias password <<< "$result"
local user hostport host port
user="${target%%@*}"; hostport="${target#*@}"
if [[ "$hostport" == *:* ]]; then
host="${hostport%%:*}"; port="${hostport##*:}"
else
host="$hostport"; port="22"
fi
[[ -z "$alias" || -z "$user" || -z "$host" ]] && {
zenity --error --text="user@host and alias are required" 2>/dev/null || true
return 1
}
printf '%s|%s|%s|%s||%s|%s' "$alias" "$user" "$host" "$port" "$alias" "$password"
}
# ── TUI mode (fzf with execute+reload — stays inside fzf on add/delete) ──────
_tui_mode() {
local pass="$1"
local passfile="$_TMP/.pass"
local list_sh="$_TMP/list.sh"
local add_sh="$_TMP/add.sh"
local del_sh="$_TMP/del.sh"
local prev_sh="$_TMP/prev.sh"
# Secure passphrase file — readable only by owner
printf '%s' "$pass" > "$passfile"
chmod 600 "$passfile"
# Snapshot constants into local vars for script generation
local S="$STORE" I="$ITERS" V="$VERSION"
# ── list.sh: decrypt + format for fzf display ────────────────────────────
{ printf '#!/usr/bin/env bash\n'
printf '_P=$(cat %q); _S=%q; _I=%q\n' "$passfile" "$S" "$I"
cat << 'LIST'
AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
-in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
| awk -F'|' '!/^#/ && NF>0 {
addr=$2"@"$3; if($4!=""&&$4!="22") addr=addr":"$4
line=$1" \342\200\224 "addr
if($7!="") line=line" [*]"
if($6!="") line=line" \342\200\224 "$6
print line
}' 2>/dev/null || true
LIST
} > "$list_sh"; chmod +x "$list_sh"
# ── add.sh: full-screen TUI form (alternate screen, hidden password) ────────
{ printf '#!/usr/bin/env bash\n'
printf '_P=$(cat %q); _S=%q; _I=%q; _V=%q\n' "$passfile" "$S" "$I" "$V"
cat << 'ADD'
# ── terminal setup ────────────────────────────────────────────────────────────
_stty=$(stty -g 2>/dev/null)
_cleanup() { stty "$_stty" 2>/dev/null; tput rmcup 2>/dev/null; tput cnorm 2>/dev/null; }
trap '_cleanup; exit 1' INT TERM
trap '_cleanup' EXIT
tput smcup 2>/dev/null # switch to alternate screen
tput civis # hide cursor while drawing
tput clear
cols=$(tput cols 2>/dev/null || printf '80')
rows=$(tput lines 2>/dev/null || printf '24')
BW=66; BH=14
BX=$(( (cols - BW) / 2 )); [[ $BX -lt 0 ]] && BX=0
BY=$(( (rows - BH) / 2 )); [[ $BY -lt 0 ]] && BY=0
# colour / style shortcuts
_b=$(tput bold); _r=$(tput sgr0); _di=$(tput dim)
_HL=$'\033[38;2;228;0;70m' # Highlights #E40046
_DK=$'\033[38;2;80;24;221m' # Dark #5018DD
_QW=$'\033[38;2;214;171;171m' # Quasi-White #D6ABAB
_RH=$'\033[38;2;245;5;5m' # Red-Hivis #F50505
# ── draw box ──────────────────────────────────────────────────────────────────
tput cup $BY $BX
printf "${_b}${_HL}╔$(printf '═%.0s' $(seq 1 $((BW-2))))╗${_r}"
for (( i=1; i<BH-1; i++ )); do
tput cup $((BY+i)) $BX; printf "${_HL}║${_r}"
tput cup $((BY+i)) $((BX+BW-1)); printf "${_HL}║${_r}"
done
tput cup $((BY+BH-1)) $BX
printf "${_b}${_HL}╚$(printf '═%.0s' $(seq 1 $((BW-2))))╝${_r}"
_title=" Add SSH Server "
tput cup $BY $(( BX + (BW - ${#_title}) / 2 ))
printf "${_b}${_HL}${_title}${_r}"
# ── layout constants ──────────────────────────────────────────────────────────
# Columns
_LC=$((BX+3)); _LF=$((BX+13)) # left label col / field col
_RC=$((BX+37)); _RF=$((BX+43)) # right label col / field col
_LFW=22; _RFW=18 # field widths
# Rows
_R1=$((BY+3)) # Username | Host
_R2=$((BY+4)) # Password | Port
_R3=$((BY+6)) # Alias
_AFW=48 # alias field width
# draw one field slot: "▸ ·····"
_slot() {
local row=$1 col=$2 w=$3
tput cup "$row" "$col"
printf "${_DK}▸ ${_di}$(printf '·%.0s' $(seq 1 "$w"))${_r}"
}
# draw labels
tput cup $_R1 $_LC; printf "${_QW}${_b}Username${_r}"
tput cup $_R2 $_LC; printf "${_QW}${_b}Password${_r}"
tput cup $_R3 $_LC; printf "${_QW}${_b}Alias${_r}"
tput cup $_R1 $_RC; printf "${_QW}${_b}Host${_r}"
tput cup $_R2 $_RC; printf "${_QW}${_b}Port${_r}"
# draw field slots
_slot $_R1 $_LF $_LFW
_slot $_R1 $_RF $_RFW
_slot $_R2 $_LF $_LFW
_slot $_R2 $_RF 5
_slot $_R3 $_LF $_AFW
# hint line
tput cup $((BY+BH-3)) $((BX+3))
printf "${_QW}${_di}Tab/Enter to advance · Ctrl-C to cancel${_r}"
# ── field reader helpers ──────────────────────────────────────────────────────
# Reads chars until Tab or Enter; Backspace edits in place; sets named var.
_readfield() {
local _v="$1" _r="" _ch
while IFS= read -r -s -n1 _ch; do
case "$_ch" in
$'\t'|$'\n'|'') break ;;
$'\x7f'|$'\x08') [[ -n "$_r" ]] && { _r="${_r%?}"; printf '\b \b'; } ;;
*) _r+="$_ch"; printf '%s' "$_ch" ;;
esac
done
printf -v "$_v" '%s' "$_r"
}
_readpw() {
local _v="$1" _r="" _ch
while IFS= read -r -s -n1 _ch; do
case "$_ch" in
$'\t'|$'\n'|'') break ;;
$'\x7f'|$'\x08') [[ -n "$_r" ]] && { _r="${_r%?}"; printf '\b \b'; } ;;
*) _r+="$_ch"; printf '*' ;;
esac
done
printf -v "$_v" '%s' "$_r"
}
# ── read fields ───────────────────────────────────────────────────────────────
tput cnorm
_user="" _host="" _pw="" _port="" _alias=""
tput cup $_R1 $((_LF+2)); _readfield _user
tput cup $_R1 $((_RF+2)); _readfield _host
tput cup $_R2 $((_LF+2)); _readpw _pw
tput cup $_R2 $((_RF+2)); printf "22"; tput cup $_R2 $((_RF+2)); _readfield _port
tput cup $_R3 $((_LF+2)); _readfield _alias
[[ -z "$_port" ]] && _port="22"
tput civis
# ── validate ──────────────────────────────────────────────────────────────────
_err() {
tput cup $((BY+BH-2)) $((BX+3))
printf "${_RH}${_b}✗ %s${_r}" "$1"
sleep 1.5; exit 1
}
[[ -z "$_user" ]] && _err "username is required"
[[ -z "$_host" ]] && _err "host is required"
[[ -z "$_alias" ]] && _err "alias is required"
_ex=$(AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
-in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
| grep -v '^#' | grep -v '^[[:space:]]*$' || true)
if printf '%s\n' "$_ex" | grep -q "^${_alias}|" 2>/dev/null; then
_err "alias \"${_alias}\" already exists"
fi
# ── save ──────────────────────────────────────────────────────────────────────
{ printf '%s\n' "$_V"
printf '%s\n' "$_ex"
printf '%s|%s|%s|%s||%s|%s\n' "$_alias" "$_user" "$_host" "$_port" "$_alias" "$_pw"
} | grep -v '^[[:space:]]*$' \
| AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" \
-out "$_S" -pass env:AMSSH_PASS
tput cup $((BY+BH-2)) $((BX+3))
printf "${_QW}${_b}✓ Added:${_r} %s → %s@%s:%s" "$_alias" "$_user" "$_host" "$_port"
sleep 0.8
ADD
} > "$add_sh"; chmod +x "$add_sh"
# ── del.sh: delete entry by alias ────────────────────────────────────────
{ printf '#!/usr/bin/env bash\n'
printf '_P=$(cat %q); _S=%q; _I=%q; _V=%q\n' "$passfile" "$S" "$I" "$V"
cat << 'DEL'
_alias="$1"; [[ -z "$_alias" ]] && exit 0
_ex=$(AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
-in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
| grep -v '^#' | grep -v '^[[:space:]]*$' || true)
{ printf '%s\n' "$_V"
printf '%s\n' "$_ex" | grep -v "^${_alias}|"
} | grep -v '^[[:space:]]*$' \
| AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" \
-out "$_S" -pass env:AMSSH_PASS
printf ' Deleted: %s\n' "$_alias" >&2
DEL
} > "$del_sh"; chmod +x "$del_sh"
# ── prev.sh: preview panel ────────────────────────────────────────────────
{ printf '#!/usr/bin/env bash\n'
printf '_P=$(cat %q); _S=%q; _I=%q\n' "$passfile" "$S" "$I"
cat << 'PREV'
_line="$1"
_a=$(printf '%s' "$_line" | awk -F' \342\200\224 ' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$1);print $1}')
[[ -z "$_a" ]] && exit 0
AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
-in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
| awk -F'|' -v a="$_a" '$1==a {
addr=$3; if($4!=""&&$4!="22") addr=addr":"$4
printf " Alias : %s\n User : %s\n Host : %s\n Port : %s\n Identity : %s\n Password : %s\n Note : %s\n",
$1,$2,addr,($4?$4:"22"),($5?$5:"(default)"),($7?"stored":"none"),($6?$6:"-")
}' 2>/dev/null || true
PREV
} > "$prev_sh"; chmod +x "$prev_sh"
# ── fzf loop: stay open after SSH session ends ────────────────────────────
while true; do
local sel
sel=$(bash "$list_sh" \
| fzf \
--prompt="SSH " \
--color='fg:#D6ABAB,hl:#E40046,fg+:#D6ABAB,bg+:#1A1A1A,hl+:#F50505,info:#5018DD,prompt:#E40046,pointer:#E40046,marker:#5018DD,spinner:#E40046,header:#D6ABAB' \
--header=$' \e[1m\e[38;2;228;0;70mEnter\e[0m connect \e[1m\e[38;2;228;0;70ma\e[0m add \e[1m\e[38;2;228;0;70md\e[0m delete \e[1m\e[38;2;228;0;70mq\e[0m quit\n \e[2m\e[38;2;80;24;221mj/k\e[0m down/up \e[2m\e[38;2;80;24;221mg/G\e[0m top/bottom \e[2m\e[38;2;80;24;221m/\e[0m filter' \
--bind="a:execute(bash '$add_sh')+reload(bash '$list_sh')" \
--bind="d:execute(bash '$del_sh' {1})+reload(bash '$list_sh')" \
--preview="bash '$prev_sh' {}" \
--preview-window="right:50%:wrap" \
--no-sort --ansi \
--bind="j:down,k:up,g:first,G:last,/:toggle-search,q:abort" \
2>/dev/null || true)
[[ -z "$sel" ]] && break
local alias
alias=$(printf '%s' "$sel" \
| awk -F' \342\200\224 ' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$1); print $1}')
[[ -z "$alias" ]] && break
local entry
entry=$(AMSSH_PASS="$pass" openssl enc -aes-256-cbc -pbkdf2 -iter "$ITERS" -d \
-in "$STORE" -pass env:AMSSH_PASS 2>/dev/null \
| awk -F'|' -v a="$alias" '$1==a' || true)
[[ -z "$entry" ]] && { printf '[amssh] Entry not found: %s\n' "$alias" >&2; continue; }
_connect "$entry"
# fzf reopens after SSH session ends
done
}
# ── drun mode (wofi) ──────────────────────────────────────────────────────────
_drun_mode() {
local pass="$1"
local raw_entries display_list
raw_entries=$(_load_entries "$pass")
if [[ -z "$raw_entries" ]]; then
display_list="(no entries — add via amssh --tui)"
else
display_list=$(printf '%s\n' "$raw_entries" | _entry_to_display)
fi
local selected
selected=$(printf '%s\n' "$display_list" \
| wofi --show=dmenu --prompt="SSH: " --dmenu \
2>/dev/null) || exit 0
[[ -z "$selected" || "$selected" == "(no entries"* ]] && exit 0
local alias
alias=$(printf '%s' "$selected" | awk -F' — ' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",\$1); print $1}')
local entry
entry=$(printf '%s\n' "$raw_entries" | awk -F'|' -v a="$alias" '$1==a')
[[ -z "$entry" ]] && exit 1
local e_alias e_user e_host e_port e_identity e_desc
IFS='|' read -r e_alias e_user e_host e_port e_identity e_desc <<< "$entry"
local args=()
[[ -n "$e_port" && "$e_port" != "22" ]] && args+=(-p "$e_port")
[[ -n "$e_identity" ]] && args+=(-i "$e_identity")
args+=("${e_user}@${e_host}")
# Launch SSH in a terminal window
"$TERM_CMD" -e ssh "${args[@]}"
}
# ── FIDO2 info on startup (TUI only) ─────────────────────────────────────────
_print_fido_status() {
[[ "$MODE" != "tui" ]] && return
if _fido_pam_available; then
if [[ "${AMSSH_PAM:-}" == "1" ]]; then
printf '[amssh] FIDO2/PAM: active (second factor enabled)\n' >&2
else
printf '[amssh] FIDO2/PAM: available — set AMSSH_PAM=1 to require as second factor\n' >&2
fi
fi
}
# ── auth config (persisted choice from first-launch dialog) ──────────────────
_load_auth_config() {
[[ ! -f "$AUTH_CONF" ]] && return
local method
method=$(cat "$AUTH_CONF")
if [[ "$method" == "fido" ]] && _fido_pam_available; then
export AMSSH_PAM=1
fi
}
_first_launch_dialog() {
mkdir -p "$CONF_DIR"
# FIDO2 hardware unavailable — silently default to passphrase
if ! _fido_hardware_available; then
printf 'passphrase' > "$AUTH_CONF"
return
fi
local choice
if [[ "$MODE" == "drun" ]]; then
choice=$(zenity --list \
--radiolist \
--title="amssh — Authentication Setup" \
--text="Choose how to unlock your SSH store:" \
--column="" --column="Method" --column="Description" \
TRUE "passphrase" "Master passphrase (recommended)" \
FALSE "fido" "FIDO2 hardware key (second factor)" \
--height=210 --width=500 2>/dev/null) || choice="passphrase"
else
# whiptail writes selection to stderr; swap fds so $() captures it
choice=$(whiptail \
--title "amssh — Authentication Setup" \
--menu "Choose how to unlock your SSH store:" \
13 62 2 \
"passphrase" "Master passphrase" \
"fido" "FIDO2 hardware key (second factor)" \
3>&1 1>&2 2>&3) || choice="passphrase"
fi
[[ -z "$choice" ]] && choice="passphrase"
printf '%s' "$choice" > "$AUTH_CONF"
if [[ "$choice" == "fido" ]]; then
printf '\n[amssh] FIDO2 selected — setting up...\n' >&2
_register_fido_key || { printf 'passphrase' > "$AUTH_CONF"; return; }
_ensure_pam_service || { printf 'passphrase' > "$AUTH_CONF"; return; }
printf '[amssh] FIDO2 ready — your key will be required on each unlock.\n' >&2
fi
}
# ── main ─────────────────────────────────────────────────────────────────────
main() {
_load_auth_config
if [[ ! -f "$STORE" ]]; then
_first_launch_dialog
_load_auth_config # apply choice made in dialog
fi
_print_fido_status
local pass
pass=$(_get_passphrase)
if [[ ! -f "$STORE" ]]; then
printf '[amssh] Creating new store at %s\n' "$STORE" >&2
local confirm
if [[ "$MODE" == "drun" ]]; then
confirm=$(_ask_pinentry) || { printf '[amssh] Cancelled\n' >&2; exit 1; }
else
confirm=$(_ask_tty "Confirm passphrase: ")
fi
[[ "$pass" != "$confirm" ]] && { printf '[amssh] Passphrases do not match\n' >&2; exit 1; }
_init_store "$pass"
printf '[amssh] Store initialised\n' >&2
[[ "$MODE" == "drun" ]] && exit 0
else
_verify_pass "$pass" || { printf '[amssh] Wrong passphrase or corrupted store\n' >&2; exit 1; }
fi
case "$MODE" in
tui) _tui_mode "$pass" ;;
drun) _drun_mode "$pass" ;;
esac
}
main