diff --git a/desktopenvs/hyprland/hypr-usr/binds.conf b/desktopenvs/hyprland/hypr-usr/binds.conf index c24958d..d2906ad 100644 --- a/desktopenvs/hyprland/hypr-usr/binds.conf +++ b/desktopenvs/hyprland/hypr-usr/binds.conf @@ -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, 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, Q ,exec, hyprctl hyprsunset temperature -450 bind= $mainMod CTRL, X ,exec, hyprctl hyprsunset identity diff --git a/desktopenvs/hyprland/scripts/amssh b/desktopenvs/hyprland/scripts/amssh new file mode 100755 index 0000000..816b528 --- /dev/null +++ b/desktopenvs/hyprland/scripts/amssh @@ -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 &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/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