amssh: encrypted SSH manager with fzf TUI and wofi drun mode

- AES-256-CBC store with PBKDF2-SHA256 (600k iters), passphrase auth
- FIDO2/PAM opt-in second factor via pam_u2f + pamtester
- fzf TUI: vim-style nav (j/k/g/G//), a=add, d=delete, q=quit
- Full-screen tput add form with Tab/Enter field advance, star-masked password
- SSH_ASKPASS_REQUIRE=force for stored password auth (no sshpass)
- First-launch dialog to choose passphrase vs FIDO2
- Hyprland keybind: SUPER-ALT-CTRL-S

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
The_miro 2026-05-11 11:21:56 +02:00
parent c4b9c5bf92
commit 4dbe200293
2 changed files with 602 additions and 0 deletions

View File

@ -291,6 +291,7 @@ bind= $mainMod SHIFT, X, exec, ~/.config/scripts/hyprland-toggle-touchpad.sh
bind= $mainMod CTRL, W ,exec, hyprctl hyprsunset gamma +10 bind= $mainMod CTRL, W ,exec, hyprctl hyprsunset gamma +10
bind= $mainMod CTRL, S ,exec, hyprctl hyprsunset gamma -10 bind= $mainMod CTRL, S ,exec, hyprctl hyprsunset gamma -10
bind= $mainMod ALT CTRL, S, exec, [tag +centered-L] kitty -e ~/.config/scripts/amssh
bind= $mainMod CTRL, A ,exec, hyprctl hyprsunset temperature +450 bind= $mainMod CTRL, A ,exec, hyprctl hyprsunset temperature +450
bind= $mainMod CTRL, Q ,exec, hyprctl hyprsunset temperature -450 bind= $mainMod CTRL, Q ,exec, hyprctl hyprsunset temperature -450
bind= $mainMod CTRL, X ,exec, hyprctl hyprsunset identity bind= $mainMod CTRL, X ,exec, hyprctl hyprsunset identity

View File

@ -0,0 +1,601 @@
#!/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 ────────────────────────────────────────────────────
_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_pam_available() {
_find_pam_u2f >/dev/null && command -v pamtester &>/dev/null
}
_pam_authenticate() {
pamtester login "$USER" authenticate 2>/dev/null
}
# ── 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)
_cy=$(tput setaf 6); _gr=$(tput setaf 2)
_re=$(tput setaf 1); _di=$(tput dim); _ye=$(tput setaf 3)
# ── draw box ──────────────────────────────────────────────────────────────────
tput cup $BY $BX
printf "${_b}${_cy}╔$(printf '═%.0s' $(seq 1 $((BW-2))))╗${_r}"
for (( i=1; i<BH-1; i++ )); do
tput cup $((BY+i)) $BX; printf "${_cy}║${_r}"
tput cup $((BY+i)) $((BX+BW-1)); printf "${_cy}║${_r}"
done
tput cup $((BY+BH-1)) $BX
printf "${_b}${_cy}╚$(printf '═%.0s' $(seq 1 $((BW-2))))╝${_r}"
_title=" Add SSH Server "
tput cup $BY $(( BX + (BW - ${#_title}) / 2 ))
printf "${_b}${_cy}${_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 "${_cy}▸ ${_di}$(printf '·%.0s' $(seq 1 "$w"))${_r}"
}
# draw labels
tput cup $_R1 $_LC; printf "${_b}Username${_r}"
tput cup $_R2 $_LC; printf "${_b}Password${_r}"
tput cup $_R3 $_LC; printf "${_b}Alias${_r}"
tput cup $_R1 $_RC; printf "${_b}Host${_r}"
tput cup $_R2 $_RC; printf "${_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 "${_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 "${_re}${_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 "${_gr}${_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 " \
--header=$' \e[1mEnter\e[0m connect \e[1ma\e[0m add \e[1md\e[0m delete \e[1mq\e[0m quit\n \e[2mj/k\e[0m down/up \e[2mg/G\e[0m top/bottom \e[2m/\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 unavailable — silently default to passphrase
if ! _fido_pam_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 — your key will be required on each unlock.\n' >&2
printf '[amssh] Ensure pam_u2f is configured for your user (pamu2fcfg).\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