From 087d6fbc558c0e345d2d1cd922b91190b257773c Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Thu, 26 Feb 2026 09:37:20 -0500 Subject: [PATCH] Format the bash script --- terminalphone.sh | 4994 +++++++++++++++++++++++----------------------- 1 file changed, 2499 insertions(+), 2495 deletions(-) diff --git a/terminalphone.sh b/terminalphone.sh index d87866f..01b65ed 100644 --- a/terminalphone.sh +++ b/terminalphone.sh @@ -29,7 +29,6 @@ NONCE_LOG_FILE="$DATA_DIR/run/nonces_$$" AUTO_LISTEN_FLAG="$DATA_DIR/run/autolisten_$$" AUTO_LISTEN_PID="" - # Defaults LISTEN_PORT=7777 TOR_SOCKS_PORT=9050 @@ -49,13 +48,13 @@ EXCLUDE_NODES="" # Tor ExcludeNodes (comma-separated country codes, e.g. {U HMAC_AUTH=0 # HMAC-sign all protocol messages (off by default) # Custom voice effect parameters (used when VOICE_EFFECT=custom) -VOICE_PITCH=0 # Pitch shift in cents (-600 to +600, 0=off) -VOICE_OVERDRIVE=0 # Overdrive gain (0=off, 5-20) -VOICE_FLANGER=0 # Flanger (0=off, 1=on) -VOICE_ECHO_DELAY=0 # Echo delay in ms (0=off, 20-200) -VOICE_ECHO_DECAY=5 # Echo decay (1-9 → 0.1-0.9) -VOICE_HIGHPASS=0 # Highpass filter freq in Hz (0=off, 300-2000) -VOICE_TREMOLO=0 # Tremolo speed in Hz (0=off, 5-40) +VOICE_PITCH=0 # Pitch shift in cents (-600 to +600, 0=off) +VOICE_OVERDRIVE=0 # Overdrive gain (0=off, 5-20) +VOICE_FLANGER=0 # Flanger (0=off, 1=on) +VOICE_ECHO_DELAY=0 # Echo delay in ms (0=off, 20-200) +VOICE_ECHO_DECAY=5 # Echo decay (1-9 → 0.1-0.9) +VOICE_HIGHPASS=0 # Highpass filter freq in Hz (0=off, 300-2000) +VOICE_TREMOLO=0 # Tremolo speed in Hz (0=off, 5-40) # ANSI Colors RED='\033[0;31m' @@ -76,7 +75,7 @@ TOR_PURPLE='\033[38;2;125;70;152m' # Platform detection IS_TERMUX=0 if [ -n "${TERMUX_VERSION:-}" ] || { [ -n "${PREFIX:-}" ] && [[ "${PREFIX:-}" == *"com.termux"* ]]; }; then - IS_TERMUX=1 + IS_TERMUX=1 fi # State @@ -90,138 +89,138 @@ ORIGINAL_STTY="" #============================================================================= cleanup() { - # Restore terminal - if [ -n "$ORIGINAL_STTY" ]; then - stty "$ORIGINAL_STTY" 2>/dev/null || true - fi - stty sane 2>/dev/null || true + # Restore terminal + if [ -n "$ORIGINAL_STTY" ]; then + stty "$ORIGINAL_STTY" 2>/dev/null || true + fi + stty sane 2>/dev/null || true - # Kill background processes - kill_bg_processes + # Kill background processes + kill_bg_processes - # Remove temp files - rm -f "$PTT_FLAG" "$CONNECTED_FLAG" "$RECV_PIPE" "$SEND_PIPE" - rm -rf "$AUDIO_DIR" 2>/dev/null || true + # Remove temp files + rm -f "$PTT_FLAG" "$CONNECTED_FLAG" "$RECV_PIPE" "$SEND_PIPE" + rm -rf "$AUDIO_DIR" 2>/dev/null || true - echo -e "\n${GREEN}${APP_NAME} shut down cleanly.${NC}" + echo -e "\n${GREEN}${APP_NAME} shut down cleanly.${NC}" } kill_bg_processes() { - # Kill any child processes - local pids - pids=$(jobs -p 2>/dev/null) || true - if [ -n "$pids" ]; then - kill $pids 2>/dev/null || true - wait $pids 2>/dev/null || true - fi + # Kill any child processes + local pids + pids=$(jobs -p 2>/dev/null) || true + if [ -n "$pids" ]; then + kill $pids 2>/dev/null || true + wait $pids 2>/dev/null || true + fi - # Kill stored PIDs - if [ -d "$PID_DIR" ]; then - for pidfile in "$PID_DIR"/*.pid; do - [ -f "$pidfile" ] || continue - local pid - pid=$(cat "$pidfile" 2>/dev/null) || continue - kill "$pid" 2>/dev/null || true - done - rm -f "$PID_DIR"/*.pid 2>/dev/null || true - fi + # Kill stored PIDs + if [ -d "$PID_DIR" ]; then + for pidfile in "$PID_DIR"/*.pid; do + [ -f "$pidfile" ] || continue + local pid + pid=$(cat "$pidfile" 2>/dev/null) || continue + kill "$pid" 2>/dev/null || true + done + rm -f "$PID_DIR"/*.pid 2>/dev/null || true + fi - # Kill our Tor instance if running - if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then - kill "$TOR_PID" 2>/dev/null || true - fi + # Kill our Tor instance if running + if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then + kill "$TOR_PID" 2>/dev/null || true + fi } save_pid() { - local name="$1" pid="$2" - mkdir -p "$PID_DIR" - echo "$pid" > "$PID_DIR/${name}.pid" + local name="$1" pid="$2" + mkdir -p "$PID_DIR" + echo "$pid" >"$PID_DIR/${name}.pid" } log_info() { - echo -e "${CYAN}[INFO]${NC} $1" + echo -e "${CYAN}[INFO]${NC} $1" } log_ok() { - echo -e "${GREEN}[ OK]${NC} $1" + echo -e "${GREEN}[ OK]${NC} $1" } log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" + echo -e "${YELLOW}[WARN]${NC} $1" } log_err() { - echo -e "${RED}[FAIL]${NC} $1" + echo -e "${RED}[FAIL]${NC} $1" } uid() { - head -c6 /dev/urandom | od -An -tx1 | tr -d ' \n' + head -c6 /dev/urandom | od -An -tx1 | tr -d ' \n' } load_config() { - if [ -f "$CONFIG_FILE" ]; then - source "$CONFIG_FILE" - fi - if [ -f "$SECRET_FILE" ]; then - # Check if the file is OpenSSL-encrypted (starts with "Salted__") - local magic - magic=$(head -c 8 "$SECRET_FILE" 2>/dev/null | cat -v) - if [[ "$magic" == "Salted__"* ]]; then - # Encrypted secret — prompt for passphrase - echo -ne " ${BOLD}Enter passphrase to unlock shared secret: ${NC}" - read -rs _unlock_pass - echo "" - if [ -n "$_unlock_pass" ]; then - SHARED_SECRET=$(openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 \ - -pass "fd:3" -in "$SECRET_FILE" 3<<< "${_unlock_pass}" 2>/dev/null) || true - if [ -z "$SHARED_SECRET" ]; then - log_warn "Failed to unlock secret (wrong passphrase?)" - log_info "You can re-enter the secret with option 4" - else - log_ok "Shared secret unlocked" - fi - else - log_warn "No passphrase entered — secret not loaded" - SHARED_SECRET="" - fi + if [ -f "$CONFIG_FILE" ]; then + source "$CONFIG_FILE" + fi + if [ -f "$SECRET_FILE" ]; then + # Check if the file is OpenSSL-encrypted (starts with "Salted__") + local magic + magic=$(head -c 8 "$SECRET_FILE" 2>/dev/null | cat -v) + if [[ "$magic" == "Salted__"* ]]; then + # Encrypted secret — prompt for passphrase + echo -ne " ${BOLD}Enter passphrase to unlock shared secret: ${NC}" + read -rs _unlock_pass + echo "" + if [ -n "$_unlock_pass" ]; then + SHARED_SECRET=$(openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 \ + -pass "fd:3" -in "$SECRET_FILE" 3<<<"${_unlock_pass}" 2>/dev/null) || true + if [ -z "$SHARED_SECRET" ]; then + log_warn "Failed to unlock secret (wrong passphrase?)" + log_info "You can re-enter the secret with option 4" else - # Plaintext secret (legacy) — load directly - SHARED_SECRET=$(cat "$SECRET_FILE") - if [ -n "$SHARED_SECRET" ]; then - log_info "Plaintext secret detected" - echo -ne " ${BOLD}Protect it with a passphrase? [Y/n]: ${NC}" - read -r _migrate - if [ "$_migrate" != "n" ] && [ "$_migrate" != "N" ]; then - echo -ne " ${BOLD}Choose a passphrase: ${NC}" - read -rs _new_pass - echo "" - if [ -n "$_new_pass" ]; then - echo -ne " ${BOLD}Confirm passphrase: ${NC}" - read -rs _confirm_pass - echo "" - if [ "$_new_pass" = "$_confirm_pass" ]; then - echo -n "$SHARED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \ - -pass "fd:3" -out "$SECRET_FILE" 3<<< "${_new_pass}" 2>/dev/null - chmod 600 "$SECRET_FILE" - log_ok "Secret encrypted with passphrase" - else - log_warn "Passphrases don't match — keeping plaintext" - fi - else - log_warn "Empty passphrase — keeping plaintext" - fi - fi - fi + log_ok "Shared secret unlocked" fi - else + else + log_warn "No passphrase entered — secret not loaded" SHARED_SECRET="" + fi + else + # Plaintext secret (legacy) — load directly + SHARED_SECRET=$(cat "$SECRET_FILE") + if [ -n "$SHARED_SECRET" ]; then + log_info "Plaintext secret detected" + echo -ne " ${BOLD}Protect it with a passphrase? [Y/n]: ${NC}" + read -r _migrate + if [ "$_migrate" != "n" ] && [ "$_migrate" != "N" ]; then + echo -ne " ${BOLD}Choose a passphrase: ${NC}" + read -rs _new_pass + echo "" + if [ -n "$_new_pass" ]; then + echo -ne " ${BOLD}Confirm passphrase: ${NC}" + read -rs _confirm_pass + echo "" + if [ "$_new_pass" = "$_confirm_pass" ]; then + echo -n "$SHARED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \ + -pass "fd:3" -out "$SECRET_FILE" 3<<<"${_new_pass}" 2>/dev/null + chmod 600 "$SECRET_FILE" + log_ok "Secret encrypted with passphrase" + else + log_warn "Passphrases don't match — keeping plaintext" + fi + else + log_warn "Empty passphrase — keeping plaintext" + fi + fi + fi fi + else + SHARED_SECRET="" + fi } save_config() { - mkdir -p "$DATA_DIR" - cat > "$CONFIG_FILE" << EOF + mkdir -p "$DATA_DIR" + cat >"$CONFIG_FILE" </dev/null + command -v "$1" &>/dev/null } install_deps() { - echo -e "\n${BOLD}${CYAN}═══ Dependency Installer ═══${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Dependency Installer ═══${NC}\n" - local deps_needed=() - local all_deps - local pkg_names_apt="tor opus-tools sox socat openssl alsa-utils" - local pkg_names_dnf="tor opus-tools sox socat openssl alsa-utils" - local pkg_names_pacman="tor opus-tools sox socat openssl alsa-utils" - local pkg_names_pkg="tor opus-tools sox socat openssl-tool ffmpeg termux-api" + local deps_needed=() + local all_deps + local pkg_names_apt="tor opus-tools sox socat openssl alsa-utils" + local pkg_names_dnf="tor opus-tools sox socat openssl alsa-utils" + local pkg_names_pacman="tor opus-tools sox socat openssl alsa-utils" + local pkg_names_pkg="tor opus-tools sox socat openssl-tool ffmpeg termux-api" - # Shared deps + platform-specific - if [ $IS_TERMUX -eq 1 ]; then - all_deps=(tor opusenc opusdec sox socat openssl ffmpeg termux-microphone-record) + # Shared deps + platform-specific + if [ $IS_TERMUX -eq 1 ]; then + all_deps=(tor opusenc opusdec sox socat openssl ffmpeg termux-microphone-record) + else + all_deps=(tor opusenc opusdec sox socat openssl arecord aplay) + fi + + # Check which deps are missing + for dep in "${all_deps[@]}"; do + if check_dep "$dep"; then + log_ok "$dep found" else - all_deps=(tor opusenc opusdec sox socat openssl arecord aplay) + deps_needed+=("$dep") + log_warn "$dep NOT found" fi + done - # Check which deps are missing - for dep in "${all_deps[@]}"; do - if check_dep "$dep"; then - log_ok "$dep found" - else - deps_needed+=("$dep") - log_warn "$dep NOT found" - fi - done + if [ ${#deps_needed[@]} -eq 0 ]; then + echo -e "\n${GREEN}All dependencies are installed!${NC}" + return 0 + fi - if [ ${#deps_needed[@]} -eq 0 ]; then - echo -e "\n${GREEN}All dependencies are installed!${NC}" - return 0 - fi + echo -e "\n${YELLOW}Missing dependencies: ${deps_needed[*]}${NC}" + echo -ne "\n${BOLD}Install missing dependencies? [Y/n]: ${NC}" + read -r _install_confirm + if [ "$_install_confirm" = "n" ] || [ "$_install_confirm" = "N" ]; then + echo -e "\n ${YELLOW}Installation skipped.${NC}" + return 1 + fi + echo "" - echo -e "\n${YELLOW}Missing dependencies: ${deps_needed[*]}${NC}" - echo -ne "\n${BOLD}Install missing dependencies? [Y/n]: ${NC}" - read -r _install_confirm - if [ "$_install_confirm" = "n" ] || [ "$_install_confirm" = "N" ]; then - echo -e "\n ${YELLOW}Installation skipped.${NC}" - return 1 - fi - echo "" + # Use sudo only if available and not on Termux + local SUDO="sudo" + if [ $IS_TERMUX -eq 1 ]; then + SUDO="" + log_info "Termux detected — installing without sudo" + elif ! check_dep sudo; then + SUDO="" + fi - # Use sudo only if available and not on Termux - local SUDO="sudo" - if [ $IS_TERMUX -eq 1 ]; then - SUDO="" - log_info "Termux detected — installing without sudo" - elif ! check_dep sudo; then - SUDO="" - fi + # Detect package manager and install + if [ $IS_TERMUX -eq 1 ]; then + log_info "Detected Termux" + log_info "Upgrading existing packages first..." + pkg upgrade -y + pkg install -y $pkg_names_pkg + echo -e "\n${YELLOW}${BOLD}NOTE:${NC} You must also install the ${BOLD}Termux:API${NC} app from F-Droid" + echo -e " for microphone access.\n" + elif check_dep apt-get; then + log_info "Detected apt package manager" + $SUDO apt-get update -qq + $SUDO apt-get install -y $pkg_names_apt + elif check_dep dnf; then + log_info "Detected dnf package manager" + $SUDO dnf install -y $pkg_names_dnf + elif check_dep pacman; then + log_info "Detected pacman package manager" + $SUDO pacman -S --noconfirm $pkg_names_pacman + else + log_err "No supported package manager found!" + log_err "Please install manually: tor, opus-tools, sox, socat, openssl, alsa-utils" + return 1 + fi - # Detect package manager and install - if [ $IS_TERMUX -eq 1 ]; then - log_info "Detected Termux" - log_info "Upgrading existing packages first..." - pkg upgrade -y - pkg install -y $pkg_names_pkg - echo -e "\n${YELLOW}${BOLD}NOTE:${NC} You must also install the ${BOLD}Termux:API${NC} app from F-Droid" - echo -e " for microphone access.\n" - elif check_dep apt-get; then - log_info "Detected apt package manager" - $SUDO apt-get update -qq - $SUDO apt-get install -y $pkg_names_apt - elif check_dep dnf; then - log_info "Detected dnf package manager" - $SUDO dnf install -y $pkg_names_dnf - elif check_dep pacman; then - log_info "Detected pacman package manager" - $SUDO pacman -S --noconfirm $pkg_names_pacman + # Verify + echo -e "\n${BOLD}Verifying installation...${NC}" + local failed=0 + for dep in "${all_deps[@]}"; do + if check_dep "$dep"; then + log_ok "$dep" else - log_err "No supported package manager found!" - log_err "Please install manually: tor, opus-tools, sox, socat, openssl, alsa-utils" - return 1 + log_err "$dep still missing!" + failed=1 fi + done - # Verify - echo -e "\n${BOLD}Verifying installation...${NC}" - local failed=0 - for dep in "${all_deps[@]}"; do - if check_dep "$dep"; then - log_ok "$dep" - else - log_err "$dep still missing!" - failed=1 - fi - done - - if [ $failed -eq 0 ]; then - echo -e "\n${GREEN}${BOLD}All dependencies installed successfully!${NC}" - else - echo -e "\n${RED}Some dependencies could not be installed.${NC}" - return 1 - fi + if [ $failed -eq 0 ]; then + echo -e "\n${GREEN}${BOLD}All dependencies installed successfully!${NC}" + else + echo -e "\n${RED}Some dependencies could not be installed.${NC}" + return 1 + fi } #============================================================================= @@ -352,10 +351,10 @@ install_deps() { #============================================================================= setup_tor() { - mkdir -p "$TOR_DIR/hidden_service" - chmod 700 "$TOR_DIR/hidden_service" + mkdir -p "$TOR_DIR/hidden_service" + chmod 700 "$TOR_DIR/hidden_service" - cat > "$TOR_CONF" << EOF + cat >"$TOR_CONF" <> "$TOR_CONF" - [ -n "$geoip6" ] && echo "GeoIPv6File $geoip6" >> "$TOR_CONF" - fi + # Locate and add GeoIP files (required for ip-to-country lookups) + local geoip="" geoip6="" + for dir in "${PREFIX:-}/share/tor" "/usr/share/tor" "/usr/local/share/tor"; do + [ -f "$dir/geoip" ] && geoip="$dir/geoip" + [ -f "$dir/geoip6" ] && geoip6="$dir/geoip6" + [ -n "$geoip" ] && break + done + if [ -n "$geoip" ]; then + echo "GeoIPFile $geoip" >>"$TOR_CONF" + [ -n "$geoip6" ] && echo "GeoIPv6File $geoip6" >>"$TOR_CONF" + fi - # Append ControlPort config if circuit display is enabled - if [ "$SHOW_CIRCUIT" -eq 1 ]; then - cat >> "$TOR_CONF" << EOF + # Append ControlPort config if circuit display is enabled + if [ "$SHOW_CIRCUIT" -eq 1 ]; then + cat >>"$TOR_CONF" <> "$TOR_CONF" << EOF + # Append ExcludeNodes if configured + if [ -n "$EXCLUDE_NODES" ]; then + cat >>"$TOR_CONF" <> "$TOR_CONF" << EOF + # Append snowflake bridge config if enabled + if [ "$SNOWFLAKE_ENABLED" -eq 1 ] && check_dep snowflake-client; then + local sf_bin + sf_bin=$(command -v snowflake-client) + cat >>"$TOR_CONF" </dev/null; then - log_info "Tor is already running (PID $TOR_PID)" - return 0 + if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then + log_info "Tor is already running (PID $TOR_PID)" + return 0 + fi + + setup_tor + + # Clear old log so we only see fresh output + local tor_log="$TOR_DIR/tor.log" + >"$tor_log" + + log_info "Starting Tor..." + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + echo -e " ${DIM}Snowflake bridge enabled — this may take longer than usual, please be patient.${NC}" + fi + tor -f "$TOR_CONF" &>/dev/null & + TOR_PID=$! + save_pid "tor" "$TOR_PID" + + # Monitor bootstrap progress from the log + local waited=0 + local timeout=120 + local last_pct="" + + while [ $waited -lt $timeout ]; do + # Check if Tor is still running + if ! kill -0 "$TOR_PID" 2>/dev/null; then + echo "" + log_err "Tor process died! Check $tor_log" + [ -f "$tor_log" ] && tail -5 "$tor_log" 2>/dev/null + return 1 fi + # Parse latest bootstrap line from log + local bootstrap_line="" + bootstrap_line=$(grep -o "Bootstrapped [0-9]*%.*" "$tor_log" 2>/dev/null | tail -1 || true) - setup_tor + if [ -n "$bootstrap_line" ]; then + local pct="" + pct=$(echo "$bootstrap_line" | grep -o '[0-9]*%' || true) + if [ -n "$pct" ] && [ "$pct" != "$last_pct" ]; then + echo -ne "\r ${DIM}${bootstrap_line}${NC} " + last_pct="$pct" + fi - # Clear old log so we only see fresh output - local tor_log="$TOR_DIR/tor.log" - > "$tor_log" - - log_info "Starting Tor..." - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - echo -e " ${DIM}Snowflake bridge enabled — this may take longer than usual, please be patient.${NC}" - fi - tor -f "$TOR_CONF" &>/dev/null & - TOR_PID=$! - save_pid "tor" "$TOR_PID" - - # Monitor bootstrap progress from the log - local waited=0 - local timeout=120 - local last_pct="" - - while [ $waited -lt $timeout ]; do - # Check if Tor is still running - if ! kill -0 "$TOR_PID" 2>/dev/null; then - echo "" - log_err "Tor process died! Check $tor_log" - [ -f "$tor_log" ] && tail -5 "$tor_log" 2>/dev/null - return 1 - fi - - # Parse latest bootstrap line from log - local bootstrap_line="" - bootstrap_line=$(grep -o "Bootstrapped [0-9]*%.*" "$tor_log" 2>/dev/null | tail -1 || true) - - if [ -n "$bootstrap_line" ]; then - local pct="" - pct=$(echo "$bootstrap_line" | grep -o '[0-9]*%' || true) - if [ -n "$pct" ] && [ "$pct" != "$last_pct" ]; then - echo -ne "\r ${DIM}${bootstrap_line}${NC} " - last_pct="$pct" - fi - - # Check for 100% - if [[ "$bootstrap_line" == *"100%"* ]]; then - echo "" - break - fi - else - # No bootstrap line yet, show waiting indicator - echo -ne "\r ${DIM}Waiting for Tor to start...${NC} " - fi - - sleep 1 - waited=$((waited + 1)) - done - - if [ $waited -ge $timeout ]; then + # Check for 100% + if [[ "$bootstrap_line" == *"100%"* ]]; then echo "" - log_err "Timed out waiting for Tor to bootstrap ($timeout seconds)" - return 1 - fi - - # Wait for onion address file (should appear quickly after 100%) - local addr_wait=0 - while [ ! -f "$ONION_FILE" ] && [ $addr_wait -lt 15 ]; do - sleep 1 - addr_wait=$((addr_wait + 1)) - done - - if [ -f "$ONION_FILE" ]; then - local onion - onion=$(cat "$ONION_FILE") - log_ok "Tor hidden service active" - echo -e " ${BOLD}${GREEN}Your address: ${WHITE}${onion}${NC}" - return 0 + break + fi else - log_err "Tor bootstrapped but hidden service address not found" - return 1 + # No bootstrap line yet, show waiting indicator + echo -ne "\r ${DIM}Waiting for Tor to start...${NC} " fi + + sleep 1 + waited=$((waited + 1)) + done + + if [ $waited -ge $timeout ]; then + echo "" + log_err "Timed out waiting for Tor to bootstrap ($timeout seconds)" + return 1 + fi + + # Wait for onion address file (should appear quickly after 100%) + local addr_wait=0 + while [ ! -f "$ONION_FILE" ] && [ $addr_wait -lt 15 ]; do + sleep 1 + addr_wait=$((addr_wait + 1)) + done + + if [ -f "$ONION_FILE" ]; then + local onion + onion=$(cat "$ONION_FILE") + log_ok "Tor hidden service active" + echo -e " ${BOLD}${GREEN}Your address: ${WHITE}${onion}${NC}" + return 0 + else + log_err "Tor bootstrapped but hidden service address not found" + return 1 + fi } stop_tor() { - stop_auto_listener - if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then - kill "$TOR_PID" 2>/dev/null || true - wait "$TOR_PID" 2>/dev/null || true - TOR_PID="" - log_ok "Tor stopped" - else - log_info "Tor is not running" - fi + stop_auto_listener + if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then + kill "$TOR_PID" 2>/dev/null || true + wait "$TOR_PID" 2>/dev/null || true + TOR_PID="" + log_ok "Tor stopped" + else + log_info "Tor is not running" + fi } get_onion() { - if [ -f "$ONION_FILE" ]; then - cat "$ONION_FILE" - else - echo "" - fi + if [ -f "$ONION_FILE" ]; then + cat "$ONION_FILE" + else + echo "" + fi } rotate_onion() { - echo -e "\n${BOLD}${CYAN}═══ Rotate Onion Address ═══${NC}\n" - local old_onion - old_onion=$(get_onion) - if [ -n "$old_onion" ]; then - echo -e " ${DIM}Current: ${old_onion}${NC}" - fi - echo -e " ${YELLOW}This will generate a new .onion address.${NC}" - echo -e " ${YELLOW}The old address will stop working.${NC}\n" - echo -ne " ${BOLD}Continue? [y/N]: ${NC}" - read -r confirm - if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then - log_info "Cancelled" - return - fi - stop_tor - rm -rf "$TOR_DIR/hidden_service" - log_info "Old hidden service keys deleted" - start_tor + echo -e "\n${BOLD}${CYAN}═══ Rotate Onion Address ═══${NC}\n" + local old_onion + old_onion=$(get_onion) + if [ -n "$old_onion" ]; then + echo -e " ${DIM}Current: ${old_onion}${NC}" + fi + echo -e " ${YELLOW}This will generate a new .onion address.${NC}" + echo -e " ${YELLOW}The old address will stop working.${NC}\n" + echo -ne " ${BOLD}Continue? [y/N]: ${NC}" + read -r confirm + if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + log_info "Cancelled" + return + fi + stop_tor + rm -rf "$TOR_DIR/hidden_service" + log_info "Old hidden service keys deleted" + start_tor } #============================================================================= @@ -585,136 +581,138 @@ rotate_onion() { # Map 2-letter country code to full name cc_to_country() { - case "${1,,}" in - ad) echo "Andorra";; ae) echo "UAE";; al) echo "Albania";; am) echo "Armenia";; - at) echo "Austria";; au) echo "Australia";; az) echo "Azerbaijan";; - ba) echo "Bosnia";; be) echo "Belgium";; bg) echo "Bulgaria";; br) echo "Brazil";; - by) echo "Belarus";; ca) echo "Canada";; ch) echo "Switzerland";; cl) echo "Chile";; - cn) echo "China";; co) echo "Colombia";; cr) echo "Costa Rica";; - cy) echo "Cyprus";; cz) echo "Czechia";; de) echo "Germany";; dk) echo "Denmark";; - dz) echo "Algeria";; ec) echo "Ecuador";; ee) echo "Estonia";; eg) echo "Egypt";; - es) echo "Spain";; fi) echo "Finland";; fr) echo "France";; - gb) echo "UK";; ge) echo "Georgia";; gr) echo "Greece";; - hk) echo "Hong Kong";; hr) echo "Croatia";; hu) echo "Hungary";; - id) echo "Indonesia";; ie) echo "Ireland";; il) echo "Israel";; in) echo "India";; - iq) echo "Iraq";; ir) echo "Iran";; is) echo "Iceland";; it) echo "Italy";; - jp) echo "Japan";; ke) echo "Kenya";; kg) echo "Kyrgyzstan";; - kr) echo "South Korea";; kz) echo "Kazakhstan";; - lb) echo "Lebanon";; li) echo "Liechtenstein";; lt) echo "Lithuania";; - lu) echo "Luxembourg";; lv) echo "Latvia";; - ma) echo "Morocco";; md) echo "Moldova";; me) echo "Montenegro";; mk) echo "N. Macedonia";; - mt) echo "Malta";; mx) echo "Mexico";; my) echo "Malaysia";; - ng) echo "Nigeria";; nl) echo "Netherlands";; no) echo "Norway";; nz) echo "New Zealand";; - pa) echo "Panama";; pe) echo "Peru";; ph) echo "Philippines";; pk) echo "Pakistan";; - pl) echo "Poland";; pt) echo "Portugal";; - ro) echo "Romania";; rs) echo "Serbia";; ru) echo "Russia";; - sa) echo "Saudi Arabia";; se) echo "Sweden";; sg) echo "Singapore";; si) echo "Slovenia";; - sk) echo "Slovakia";; th) echo "Thailand";; tn) echo "Tunisia";; tr) echo "Turkey";; - tw) echo "Taiwan";; ua) echo "Ukraine";; us) echo "USA";; - uy) echo "Uruguay";; uz) echo "Uzbekistan";; ve) echo "Venezuela";; - vn) echo "Vietnam";; za) echo "South Africa";; - *) echo "${1^^}";; - esac + case "${1,,}" in + ad) echo "Andorra" ;; ae) echo "UAE" ;; al) echo "Albania" ;; am) echo "Armenia" ;; + at) echo "Austria" ;; au) echo "Australia" ;; az) echo "Azerbaijan" ;; + ba) echo "Bosnia" ;; be) echo "Belgium" ;; bg) echo "Bulgaria" ;; br) echo "Brazil" ;; + by) echo "Belarus" ;; ca) echo "Canada" ;; ch) echo "Switzerland" ;; cl) echo "Chile" ;; + cn) echo "China" ;; co) echo "Colombia" ;; cr) echo "Costa Rica" ;; + cy) echo "Cyprus" ;; cz) echo "Czechia" ;; de) echo "Germany" ;; dk) echo "Denmark" ;; + dz) echo "Algeria" ;; ec) echo "Ecuador" ;; ee) echo "Estonia" ;; eg) echo "Egypt" ;; + es) echo "Spain" ;; fi) echo "Finland" ;; fr) echo "France" ;; + gb) echo "UK" ;; ge) echo "Georgia" ;; gr) echo "Greece" ;; + hk) echo "Hong Kong" ;; hr) echo "Croatia" ;; hu) echo "Hungary" ;; + id) echo "Indonesia" ;; ie) echo "Ireland" ;; il) echo "Israel" ;; in) echo "India" ;; + iq) echo "Iraq" ;; ir) echo "Iran" ;; is) echo "Iceland" ;; it) echo "Italy" ;; + jp) echo "Japan" ;; ke) echo "Kenya" ;; kg) echo "Kyrgyzstan" ;; + kr) echo "South Korea" ;; kz) echo "Kazakhstan" ;; + lb) echo "Lebanon" ;; li) echo "Liechtenstein" ;; lt) echo "Lithuania" ;; + lu) echo "Luxembourg" ;; lv) echo "Latvia" ;; + ma) echo "Morocco" ;; md) echo "Moldova" ;; me) echo "Montenegro" ;; mk) echo "N. Macedonia" ;; + mt) echo "Malta" ;; mx) echo "Mexico" ;; my) echo "Malaysia" ;; + ng) echo "Nigeria" ;; nl) echo "Netherlands" ;; no) echo "Norway" ;; nz) echo "New Zealand" ;; + pa) echo "Panama" ;; pe) echo "Peru" ;; ph) echo "Philippines" ;; pk) echo "Pakistan" ;; + pl) echo "Poland" ;; pt) echo "Portugal" ;; + ro) echo "Romania" ;; rs) echo "Serbia" ;; ru) echo "Russia" ;; + sa) echo "Saudi Arabia" ;; se) echo "Sweden" ;; sg) echo "Singapore" ;; si) echo "Slovenia" ;; + sk) echo "Slovakia" ;; th) echo "Thailand" ;; tn) echo "Tunisia" ;; tr) echo "Turkey" ;; + tw) echo "Taiwan" ;; ua) echo "Ukraine" ;; us) echo "USA" ;; + uy) echo "Uruguay" ;; uz) echo "Uzbekistan" ;; ve) echo "Venezuela" ;; + vn) echo "Vietnam" ;; za) echo "South Africa" ;; + *) echo "${1^^}" ;; + esac } # Query Tor control port for active circuit hops # Outputs one line per hop: "relay_name|country_name" # Returns 1 if circuit info is unavailable get_circuit_hops() { - [ "$SHOW_CIRCUIT" -eq 0 ] && return 1 + [ "$SHOW_CIRCUIT" -eq 0 ] && return 1 - local cookie_file="$TOR_DIR/data/control_auth_cookie" - [ ! -f "$cookie_file" ] && return 1 + local cookie_file="$TOR_DIR/data/control_auth_cookie" + [ ! -f "$cookie_file" ] && return 1 - local cookie_hex - cookie_hex=$(od -An -tx1 "$cookie_file" | tr -d ' \n' 2>/dev/null) || return 1 - [ -z "$cookie_hex" ] && return 1 + local cookie_hex + cookie_hex=$(od -An -tx1 "$cookie_file" | tr -d ' \n' 2>/dev/null) || return 1 + [ -z "$cookie_hex" ] && return 1 - # Step 1: Get circuit status - local circ_resp - circ_resp=$({ - printf 'AUTHENTICATE %s\r\n' "$cookie_hex" - printf 'GETINFO circuit-status\r\n' - printf 'QUIT\r\n' - } | socat - TCP:127.0.0.1:$TOR_CONTROL_PORT 2>/dev/null | tr -d '\r') || return 1 + # Step 1: Get circuit status + local circ_resp + circ_resp=$({ + printf 'AUTHENTICATE %s\r\n' "$cookie_hex" + printf 'GETINFO circuit-status\r\n' + printf 'QUIT\r\n' + } | socat - TCP:127.0.0.1:$TOR_CONTROL_PORT 2>/dev/null | tr -d '\r') || return 1 - echo "$circ_resp" | grep -q "^250 OK" || return 1 + echo "$circ_resp" | grep -q "^250 OK" || return 1 - # Find best BUILT circuit — prefer HS circuits - local circuit_line - circuit_line=$(echo "$circ_resp" | grep " BUILT " \ - | grep -E "PURPOSE=HS_SERVICE_INTRO|PURPOSE=HS_CLIENT_REND" | head -1) || true - [ -z "$circuit_line" ] && circuit_line=$(echo "$circ_resp" | grep " BUILT " | head -1) - [ -z "$circuit_line" ] && return 1 + # Find best BUILT circuit — prefer HS circuits + local circuit_line + circuit_line=$(echo "$circ_resp" | grep " BUILT " | + grep -E "PURPOSE=HS_SERVICE_INTRO|PURPOSE=HS_CLIENT_REND" | head -1) || true + [ -z "$circuit_line" ] && circuit_line=$(echo "$circ_resp" | grep " BUILT " | head -1) + [ -z "$circuit_line" ] && return 1 - # Extract path (field 3: comma-separated $FP~Name entries) - local path - path=$(echo "$circuit_line" | awk '{print $3}') - [ -z "$path" ] && return 1 + # Extract path (field 3: comma-separated $FP~Name entries) + local path + path=$(echo "$circuit_line" | awk '{print $3}') + [ -z "$path" ] && return 1 - # Parse relay names and fingerprints - local names=() fps=() - IFS=',' read -ra relays <<< "$path" - for r in "${relays[@]}"; do - local name fp - if [[ "$r" == *"~"* ]]; then - name="${r#*~}"; fp="${r%%~*}" - else - name="${r:0:8}..."; fp="$r" - fi - names+=("$name") - fps+=("${fp#\$}") + # Parse relay names and fingerprints + local names=() fps=() + IFS=',' read -ra relays <<<"$path" + for r in "${relays[@]}"; do + local name fp + if [[ "$r" == *"~"* ]]; then + name="${r#*~}" + fp="${r%%~*}" + else + name="${r:0:8}..." + fp="$r" + fi + names+=("$name") + fps+=("${fp#\$}") + done + [ ${#names[@]} -eq 0 ] && return 1 + + # Step 2: Get IPs for all relays via ns/id (single session) + local ns_resp + ns_resp=$({ + printf 'AUTHENTICATE %s\r\n' "$cookie_hex" + for fp in "${fps[@]}"; do + printf 'GETINFO ns/id/%s\r\n' "$fp" done - [ ${#names[@]} -eq 0 ] && return 1 + printf 'QUIT\r\n' + } | socat - TCP:127.0.0.1:$TOR_CONTROL_PORT 2>/dev/null | tr -d '\r') || true - # Step 2: Get IPs for all relays via ns/id (single session) - local ns_resp - ns_resp=$({ - printf 'AUTHENTICATE %s\r\n' "$cookie_hex" - for fp in "${fps[@]}"; do - printf 'GETINFO ns/id/%s\r\n' "$fp" - done - printf 'QUIT\r\n' + local ips=() + if [ -n "$ns_resp" ]; then + while IFS= read -r rline; do + ips+=("$(echo "$rline" | awk '{print $7}')") + done <<<"$(echo "$ns_resp" | grep '^r ')" + fi + + # Step 3: Resolve countries for all IPs (single session) + local countries=() + local has_ips=0 + for ip in "${ips[@]}"; do [ -n "$ip" ] && has_ips=1 && break; done + + if [ "$has_ips" -eq 1 ]; then + local cc_resp + cc_resp=$({ + printf 'AUTHENTICATE %s\r\n' "$cookie_hex" + for ip in "${ips[@]}"; do + [ -n "$ip" ] && printf 'GETINFO ip-to-country/%s\r\n' "$ip" + done + printf 'QUIT\r\n' } | socat - TCP:127.0.0.1:$TOR_CONTROL_PORT 2>/dev/null | tr -d '\r') || true - local ips=() - if [ -n "$ns_resp" ]; then - while IFS= read -r rline; do - ips+=("$(echo "$rline" | awk '{print $7}')") - done <<< "$(echo "$ns_resp" | grep '^r ')" + if [ -n "$cc_resp" ]; then + while IFS= read -r ccline; do + local cc + cc=$(echo "$ccline" | sed 's/.*=//') + countries+=("$(cc_to_country "$cc")") + done <<<"$(echo "$cc_resp" | grep 'ip-to-country')" fi + fi - # Step 3: Resolve countries for all IPs (single session) - local countries=() - local has_ips=0 - for ip in "${ips[@]}"; do [ -n "$ip" ] && has_ips=1 && break; done - - if [ "$has_ips" -eq 1 ]; then - local cc_resp - cc_resp=$({ - printf 'AUTHENTICATE %s\r\n' "$cookie_hex" - for ip in "${ips[@]}"; do - [ -n "$ip" ] && printf 'GETINFO ip-to-country/%s\r\n' "$ip" - done - printf 'QUIT\r\n' - } | socat - TCP:127.0.0.1:$TOR_CONTROL_PORT 2>/dev/null | tr -d '\r') || true - - if [ -n "$cc_resp" ]; then - while IFS= read -r ccline; do - local cc - cc=$(echo "$ccline" | sed 's/.*=//') - countries+=("$(cc_to_country "$cc")") - done <<< "$(echo "$cc_resp" | grep 'ip-to-country')" - fi - fi - - # Output one line per hop: "name|country" - local total=${#names[@]} - for ((i = 0; i < total; i++)); do - local country="${countries[$i]:-??}" - echo "${names[$i]}|${country}" - done - return 0 + # Output one line per hop: "name|country" + local total=${#names[@]} + for ((i = 0; i < total; i++)); do + local country="${countries[$i]:-??}" + echo "${names[$i]}|${country}" + done + return 0 } #============================================================================= @@ -722,75 +720,74 @@ get_circuit_hops() { #============================================================================= set_shared_secret() { - echo -e "\n${BOLD}${CYAN}═══ Set Shared Secret ═══${NC}\n" - echo -e "${DIM}Both parties must use the same secret for the call to work.${NC}" - echo -e "${DIM}Share this secret securely (in person, via encrypted message, etc.)${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Set Shared Secret ═══${NC}\n" + echo -e "${DIM}Both parties must use the same secret for the call to work.${NC}" + echo -e "${DIM}Share this secret securely (in person, via encrypted message, etc.)${NC}\n" - if [ -n "$SHARED_SECRET" ]; then - echo -e "Current secret: ${DIM}(set)${NC}" - else - echo -e "Current secret: ${DIM}(none)${NC}" - fi + if [ -n "$SHARED_SECRET" ]; then + echo -e "Current secret: ${DIM}(set)${NC}" + else + echo -e "Current secret: ${DIM}(none)${NC}" + fi - echo -ne "\n${BOLD}Enter shared secret: ${NC}" - read -r new_secret + echo -ne "\n${BOLD}Enter shared secret: ${NC}" + read -r new_secret - if [ -z "$new_secret" ]; then - log_warn "Secret not changed" + if [ -z "$new_secret" ]; then + log_warn "Secret not changed" + return + fi + + SHARED_SECRET="$new_secret" + mkdir -p "$DATA_DIR" + + echo -ne "\n ${BOLD}Protect with a passphrase? [Y/n]: ${NC}" + read -r _protect + if [ "$_protect" != "n" ] && [ "$_protect" != "N" ]; then + echo -ne " ${BOLD}Choose a passphrase: ${NC}" + read -rs _pass + echo "" + if [ -n "$_pass" ]; then + echo -ne " ${BOLD}Confirm passphrase: ${NC}" + read -rs _pass2 + echo "" + if [ "$_pass" = "$_pass2" ]; then + echo -n "$SHARED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \ + -pass "fd:3" -out "$SECRET_FILE" 3<<<"${_pass}" 2>/dev/null + chmod 600 "$SECRET_FILE" + log_ok "Shared secret saved (encrypted with passphrase)" return + else + log_warn "Passphrases don't match" + fi + else + log_warn "Empty passphrase" fi + log_info "Falling back to plaintext storage" + fi - SHARED_SECRET="$new_secret" - mkdir -p "$DATA_DIR" - - echo -ne "\n ${BOLD}Protect with a passphrase? [Y/n]: ${NC}" - read -r _protect - if [ "$_protect" != "n" ] && [ "$_protect" != "N" ]; then - echo -ne " ${BOLD}Choose a passphrase: ${NC}" - read -rs _pass - echo "" - if [ -n "$_pass" ]; then - echo -ne " ${BOLD}Confirm passphrase: ${NC}" - read -rs _pass2 - echo "" - if [ "$_pass" = "$_pass2" ]; then - echo -n "$SHARED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \ - -pass "fd:3" -out "$SECRET_FILE" 3<<< "${_pass}" 2>/dev/null - chmod 600 "$SECRET_FILE" - log_ok "Shared secret saved (encrypted with passphrase)" - return - else - log_warn "Passphrases don't match" - fi - else - log_warn "Empty passphrase" - fi - log_info "Falling back to plaintext storage" - fi - - # Plaintext fallback - echo -n "$SHARED_SECRET" > "$SECRET_FILE" - chmod 600 "$SECRET_FILE" - log_ok "Shared secret saved" + # Plaintext fallback + echo -n "$SHARED_SECRET" >"$SECRET_FILE" + chmod 600 "$SECRET_FILE" + log_ok "Shared secret saved" } - # Encrypt a file encrypt_file() { - local infile="$1" outfile="$2" - local c="$CIPHER" - [ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE") - openssl enc -"${c}" -pbkdf2 -iter 10000 -pass "fd:3" \ - -in "$infile" -out "$outfile" 3<<< "${SHARED_SECRET}" 2>/dev/null + local infile="$1" outfile="$2" + local c="$CIPHER" + [ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE") + openssl enc -"${c}" -pbkdf2 -iter 10000 -pass "fd:3" \ + -in "$infile" -out "$outfile" 3<<<"${SHARED_SECRET}" 2>/dev/null } # Decrypt a file decrypt_file() { - local infile="$1" outfile="$2" - local c="$CIPHER" - [ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE") - openssl enc -d -"${c}" -pbkdf2 -iter 10000 -pass "fd:3" \ - -in "$infile" -out "$outfile" 3<<< "${SHARED_SECRET}" 2>/dev/null + local infile="$1" outfile="$2" + local c="$CIPHER" + [ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE") + openssl enc -d -"${c}" -pbkdf2 -iter 10000 -pass "fd:3" \ + -in "$infile" -out "$outfile" 3<<<"${SHARED_SECRET}" 2>/dev/null } #============================================================================= @@ -799,52 +796,52 @@ decrypt_file() { # Send a protocol message, optionally HMAC-signed with random nonce proto_send() { - local msg="$1" - local _hmac=0 - [ -f "$HMAC_RUNTIME_FILE" ] && _hmac=$(cat "$HMAC_RUNTIME_FILE" 2>/dev/null) - [ -z "$_hmac" ] && _hmac="$HMAC_AUTH" - if [ "$_hmac" -eq 1 ]; then - local nonce sig signed_msg - nonce=$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n') - signed_msg="${nonce}:${msg}" - sig=$(printf '%s' "$signed_msg" | openssl dgst -sha256 -hmac "$SHARED_SECRET" -r 2>/dev/null | cut -d' ' -f1) - echo "${signed_msg}|${sig}" >&4 2>/dev/null || true - else - echo "$msg" >&4 2>/dev/null || true - fi + local msg="$1" + local _hmac=0 + [ -f "$HMAC_RUNTIME_FILE" ] && _hmac=$(cat "$HMAC_RUNTIME_FILE" 2>/dev/null) + [ -z "$_hmac" ] && _hmac="$HMAC_AUTH" + if [ "$_hmac" -eq 1 ]; then + local nonce sig signed_msg + nonce=$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n') + signed_msg="${nonce}:${msg}" + sig=$(printf '%s' "$signed_msg" | openssl dgst -sha256 -hmac "$SHARED_SECRET" -r 2>/dev/null | cut -d' ' -f1) + echo "${signed_msg}|${sig}" >&4 2>/dev/null || true + else + echo "$msg" >&4 2>/dev/null || true + fi } # Verify HMAC on a received message # Outputs the raw message (without nonce) on stdout; returns 1 on failure proto_verify() { - local line="$1" - local _hmac=0 - [ -f "$HMAC_RUNTIME_FILE" ] && _hmac=$(cat "$HMAC_RUNTIME_FILE" 2>/dev/null) - [ -z "$_hmac" ] && _hmac="$HMAC_AUTH" - if [ "$_hmac" -ne 1 ]; then - echo "$line" - return 0 - fi - # Must contain | separator for HMAC - if [[ "$line" != *"|"* ]]; then - return 1 - fi - local signed_msg="${line%|*}" - local received_sig="${line##*|}" - local expected_sig - expected_sig=$(printf '%s' "$signed_msg" | openssl dgst -sha256 -hmac "$SHARED_SECRET" -r 2>/dev/null | cut -d' ' -f1) - if [ "$received_sig" = "$expected_sig" ]; then - # Reject replayed nonces - local nonce="${signed_msg%%:*}" - if grep -qF "$nonce" "$NONCE_LOG_FILE" 2>/dev/null; then - return 1 - fi - echo "$nonce" >> "$NONCE_LOG_FILE" 2>/dev/null - # Strip nonce prefix (nonce:message → message) - echo "${signed_msg#*:}" - return 0 - fi + local line="$1" + local _hmac=0 + [ -f "$HMAC_RUNTIME_FILE" ] && _hmac=$(cat "$HMAC_RUNTIME_FILE" 2>/dev/null) + [ -z "$_hmac" ] && _hmac="$HMAC_AUTH" + if [ "$_hmac" -ne 1 ]; then + echo "$line" + return 0 + fi + # Must contain | separator for HMAC + if [[ "$line" != *"|"* ]]; then return 1 + fi + local signed_msg="${line%|*}" + local received_sig="${line##*|}" + local expected_sig + expected_sig=$(printf '%s' "$signed_msg" | openssl dgst -sha256 -hmac "$SHARED_SECRET" -r 2>/dev/null | cut -d' ' -f1) + if [ "$received_sig" = "$expected_sig" ]; then + # Reject replayed nonces + local nonce="${signed_msg%%:*}" + if grep -qF "$nonce" "$NONCE_LOG_FILE" 2>/dev/null; then + return 1 + fi + echo "$nonce" >>"$NONCE_LOG_FILE" 2>/dev/null + # Strip nonce prefix (nonce:message → message) + echo "${signed_msg#*:}" + return 0 + fi + return 1 } #============================================================================= @@ -853,285 +850,281 @@ proto_verify() { # Record a timed chunk of raw audio (used by audio test) audio_record() { - local outfile="$1" - local duration="${2:-$CHUNK_DURATION}" + local outfile="$1" + local duration="${2:-$CHUNK_DURATION}" - if [ $IS_TERMUX -eq 1 ]; then - local tmp_rec="$AUDIO_DIR/tmrec_$(uid).tmp" - rm -f "$tmp_rec" - termux-microphone-record -l "$((duration + 1))" -f "$tmp_rec" &>/dev/null - sleep "$duration" - termux-microphone-record -q &>/dev/null || true - sleep 0.5 - if [ -s "$tmp_rec" ]; then - ffmpeg -y -i "$tmp_rec" -f s16le -ar "$SAMPLE_RATE" -ac 1 \ - "$outfile" &>/dev/null || log_warn "ffmpeg conversion failed" - fi - rm -f "$tmp_rec" - else - arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -d "$duration" \ - -q "$outfile" 2>/dev/null + if [ $IS_TERMUX -eq 1 ]; then + local tmp_rec="$AUDIO_DIR/tmrec_$(uid).tmp" + rm -f "$tmp_rec" + termux-microphone-record -l "$((duration + 1))" -f "$tmp_rec" &>/dev/null + sleep "$duration" + termux-microphone-record -q &>/dev/null || true + sleep 0.5 + if [ -s "$tmp_rec" ]; then + ffmpeg -y -i "$tmp_rec" -f s16le -ar "$SAMPLE_RATE" -ac 1 \ + "$outfile" &>/dev/null || log_warn "ffmpeg conversion failed" fi + rm -f "$tmp_rec" + else + arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -d "$duration" \ + -q "$outfile" 2>/dev/null + fi } # Start continuous recording in background (returns immediately) # Sets REC_PID and REC_FILE globals start_recording() { - local _id=$(uid) + local _id=$(uid) - if [ $IS_TERMUX -eq 1 ]; then - REC_FILE="$AUDIO_DIR/msg_${_id}.tmp" - rm -f "$REC_FILE" - termux-microphone-record -l 120 -f "$REC_FILE" &>/dev/null & - REC_PID=$! - else - REC_FILE="$AUDIO_DIR/msg_${_id}.tmp" - arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -q "$REC_FILE" 2>/dev/null & - REC_PID=$! - fi + if [ $IS_TERMUX -eq 1 ]; then + REC_FILE="$AUDIO_DIR/msg_${_id}.tmp" + rm -f "$REC_FILE" + termux-microphone-record -l 120 -f "$REC_FILE" &>/dev/null & + REC_PID=$! + else + REC_FILE="$AUDIO_DIR/msg_${_id}.tmp" + arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -q "$REC_FILE" 2>/dev/null & + REC_PID=$! + fi } # Apply voice effect to raw PCM using sox apply_voice_effect() { - local infile="$1" - local outfile="$2" - local fmt="-t raw -r $SAMPLE_RATE -e signed -b 16 -c 1" - case "$VOICE_EFFECT" in - deep) - sox $fmt "$infile" $fmt "$outfile" pitch -400 2>/dev/null - ;; - high) - sox $fmt "$infile" $fmt "$outfile" pitch 500 2>/dev/null - ;; - robot) - sox $fmt "$infile" $fmt "$outfile" overdrive 10 flanger 2>/dev/null - ;; - echo) - sox $fmt "$infile" $fmt "$outfile" echo 0.8 0.88 60 0.4 2>/dev/null - ;; - whisper) - sox $fmt "$infile" $fmt "$outfile" highpass 1000 tremolo 20 2>/dev/null - ;; - custom) - # Build sox effects chain from individual parameters - local effects="" - [ "$VOICE_PITCH" -ne 0 ] 2>/dev/null && effects="$effects pitch $VOICE_PITCH" - [ "$VOICE_OVERDRIVE" -gt 0 ] 2>/dev/null && effects="$effects overdrive $VOICE_OVERDRIVE" - [ "$VOICE_FLANGER" -eq 1 ] 2>/dev/null && effects="$effects flanger" - [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null && effects="$effects echo 0.8 0.88 $VOICE_ECHO_DELAY 0.${VOICE_ECHO_DECAY}" - [ "$VOICE_HIGHPASS" -gt 0 ] 2>/dev/null && effects="$effects highpass $VOICE_HIGHPASS" - [ "$VOICE_TREMOLO" -gt 0 ] 2>/dev/null && effects="$effects tremolo $VOICE_TREMOLO" - if [ -n "$effects" ]; then - sox $fmt "$infile" $fmt "$outfile" $effects 2>/dev/null - else - return 1 # no effects configured - fi - ;; - *) - return 1 # no effect - ;; - esac + local infile="$1" + local outfile="$2" + local fmt="-t raw -r $SAMPLE_RATE -e signed -b 16 -c 1" + case "$VOICE_EFFECT" in + deep) + sox $fmt "$infile" $fmt "$outfile" pitch -400 2>/dev/null + ;; + high) + sox $fmt "$infile" $fmt "$outfile" pitch 500 2>/dev/null + ;; + robot) + sox $fmt "$infile" $fmt "$outfile" overdrive 10 flanger 2>/dev/null + ;; + echo) + sox $fmt "$infile" $fmt "$outfile" echo 0.8 0.88 60 0.4 2>/dev/null + ;; + whisper) + sox $fmt "$infile" $fmt "$outfile" highpass 1000 tremolo 20 2>/dev/null + ;; + custom) + # Build sox effects chain from individual parameters + local effects="" + [ "$VOICE_PITCH" -ne 0 ] 2>/dev/null && effects="$effects pitch $VOICE_PITCH" + [ "$VOICE_OVERDRIVE" -gt 0 ] 2>/dev/null && effects="$effects overdrive $VOICE_OVERDRIVE" + [ "$VOICE_FLANGER" -eq 1 ] 2>/dev/null && effects="$effects flanger" + [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null && effects="$effects echo 0.8 0.88 $VOICE_ECHO_DELAY 0.${VOICE_ECHO_DECAY}" + [ "$VOICE_HIGHPASS" -gt 0 ] 2>/dev/null && effects="$effects highpass $VOICE_HIGHPASS" + [ "$VOICE_TREMOLO" -gt 0 ] 2>/dev/null && effects="$effects tremolo $VOICE_TREMOLO" + if [ -n "$effects" ]; then + sox $fmt "$infile" $fmt "$outfile" $effects 2>/dev/null + else + return 1 # no effects configured + fi + ;; + *) + return 1 # no effect + ;; + esac } # Stop recording and send the message # Encodes, encrypts, base64-encodes, and writes to fd 4 stop_and_send() { - local _id=$(uid) - local raw_file="$AUDIO_DIR/tx_${_id}.tmp" - local opus_file="$AUDIO_DIR/tx_o_${_id}.tmp" - local enc_file="$AUDIO_DIR/tx_e_${_id}.tmp" + local _id=$(uid) + local raw_file="$AUDIO_DIR/tx_${_id}.tmp" + local opus_file="$AUDIO_DIR/tx_o_${_id}.tmp" + local enc_file="$AUDIO_DIR/tx_e_${_id}.tmp" - # Stop the recording - if [ $IS_TERMUX -eq 1 ]; then - termux-microphone-record -q &>/dev/null || true - kill "$REC_PID" 2>/dev/null || true - wait "$REC_PID" 2>/dev/null || true - sleep 0.3 # let file flush - # Convert m4a → raw PCM - if [ -s "$REC_FILE" ]; then - ffmpeg -y -i "$REC_FILE" -f s16le -ar "$SAMPLE_RATE" -ac 1 \ - "$raw_file" &>/dev/null || true - fi - rm -f "$REC_FILE" + # Stop the recording + if [ $IS_TERMUX -eq 1 ]; then + termux-microphone-record -q &>/dev/null || true + kill "$REC_PID" 2>/dev/null || true + wait "$REC_PID" 2>/dev/null || true + sleep 0.3 # let file flush + # Convert m4a → raw PCM + if [ -s "$REC_FILE" ]; then + ffmpeg -y -i "$REC_FILE" -f s16le -ar "$SAMPLE_RATE" -ac 1 \ + "$raw_file" &>/dev/null || true + fi + rm -f "$REC_FILE" + else + kill "$REC_PID" 2>/dev/null || true + wait "$REC_PID" 2>/dev/null || true + raw_file="$REC_FILE" # already in raw format + fi + + REC_PID="" + REC_FILE="" + + # Apply voice effect if set + if [ -s "$raw_file" ] && [ "$VOICE_EFFECT" != "none" ]; then + local fx_file="$AUDIO_DIR/tx_fx_${_id}.tmp" + if apply_voice_effect "$raw_file" "$fx_file"; then + mv "$fx_file" "$raw_file" else - kill "$REC_PID" 2>/dev/null || true - wait "$REC_PID" 2>/dev/null || true - raw_file="$REC_FILE" # already in raw format + rm -f "$fx_file" 2>/dev/null fi + fi - REC_PID="" - REC_FILE="" + # Encode → encrypt → send + if [ -s "$raw_file" ]; then + opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \ + --bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \ + --speech --quiet \ + "$raw_file" "$opus_file" 2>/dev/null - # Apply voice effect if set - if [ -s "$raw_file" ] && [ "$VOICE_EFFECT" != "none" ]; then - local fx_file="$AUDIO_DIR/tx_fx_${_id}.tmp" - if apply_voice_effect "$raw_file" "$fx_file"; then - mv "$fx_file" "$raw_file" - else - rm -f "$fx_file" 2>/dev/null - fi + if [ -s "$opus_file" ]; then + encrypt_file "$opus_file" "$enc_file" 2>/dev/null + if [ -s "$enc_file" ]; then + local enc_size + enc_size=$(stat -c%s "$enc_file" 2>/dev/null || echo 0) + local size_kb=$((enc_size * 10 / 1024)) + local size_whole=$((size_kb / 10)) + local size_frac=$((size_kb % 10)) + + local b64 + b64=$(base64 -w 0 "$enc_file" 2>/dev/null) + proto_send "AUDIO:${b64}" + LAST_SENT_INFO="${size_whole}.${size_frac}KB" + fi fi - - # Encode → encrypt → send - if [ -s "$raw_file" ]; then - opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \ - --bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \ - --speech --quiet \ - "$raw_file" "$opus_file" 2>/dev/null - - if [ -s "$opus_file" ]; then - encrypt_file "$opus_file" "$enc_file" 2>/dev/null - if [ -s "$enc_file" ]; then - local enc_size - enc_size=$(stat -c%s "$enc_file" 2>/dev/null || echo 0) - local size_kb=$(( enc_size * 10 / 1024 )) - local size_whole=$(( size_kb / 10 )) - local size_frac=$(( size_kb % 10 )) - - local b64 - b64=$(base64 -w 0 "$enc_file" 2>/dev/null) - proto_send "AUDIO:${b64}" - LAST_SENT_INFO="${size_whole}.${size_frac}KB" - fi - fi - fi - rm -f "$raw_file" "$opus_file" "$enc_file" 2>/dev/null + fi + rm -f "$raw_file" "$opus_file" "$enc_file" 2>/dev/null } # Play audio (platform-aware) audio_play() { - local infile="$1" - local rate="${2:-48000}" + local infile="$1" + local rate="${2:-48000}" - if [ $IS_TERMUX -eq 1 ]; then - # Termux: use sox play (avoids Android MediaPlayer indexing) - play -q -t raw -r "$rate" -e signed -b 16 -c 1 "$infile" 2>/dev/null || true - else - # Linux: use ALSA aplay - aplay -f S16_LE -r "$rate" -c 1 -q "$infile" 2>/dev/null - fi + if [ $IS_TERMUX -eq 1 ]; then + # Termux: use sox play (avoids Android MediaPlayer indexing) + play -q -t raw -r "$rate" -e signed -b 16 -c 1 "$infile" 2>/dev/null || true + else + # Linux: use ALSA aplay + aplay -f S16_LE -r "$rate" -c 1 -q "$infile" 2>/dev/null + fi } - - # Play an opus file play_chunk() { - local opus_file="$1" + local opus_file="$1" - if [ $IS_TERMUX -eq 1 ]; then - # Termux: pipe decode directly to sox play (avoids temp file + Android media framework) - opusdec --quiet --rate 48000 "$opus_file" - 2>/dev/null | \ - play -q -t raw -r 48000 -e signed -b 16 -c 1 - 2>/dev/null || true - else - # Linux: pipe decode directly to aplay - opusdec --quiet --rate 48000 "$opus_file" - 2>/dev/null | \ - aplay -f S16_LE -r 48000 -c 1 -q 2>/dev/null || true - fi + if [ $IS_TERMUX -eq 1 ]; then + # Termux: pipe decode directly to sox play (avoids temp file + Android media framework) + opusdec --quiet --rate 48000 "$opus_file" - 2>/dev/null | + play -q -t raw -r 48000 -e signed -b 16 -c 1 - 2>/dev/null || true + else + # Linux: pipe decode directly to aplay + opusdec --quiet --rate 48000 "$opus_file" - 2>/dev/null | + aplay -f S16_LE -r 48000 -c 1 -q 2>/dev/null || true + fi } - - #============================================================================= # CALL CLEANUP — RESET EVERYTHING TO FRESH STATE #============================================================================= cleanup_call() { - # Restore terminal to sane state - if [ -n "$ORIGINAL_STTY" ]; then - stty "$ORIGINAL_STTY" 2>/dev/null || true + # Restore terminal to sane state + if [ -n "$ORIGINAL_STTY" ]; then + stty "$ORIGINAL_STTY" 2>/dev/null || true + fi + stty sane 2>/dev/null || true + ORIGINAL_STTY="" + + # Close pipe file descriptors to unblock any blocking reads + # NOTE: must use { } group so 2>/dev/null doesn't permanently redirect stderr + { exec 3<&-; } 2>/dev/null || true + { exec 4>&-; } 2>/dev/null || true + + # Kill all call-related processes by PID files + for pidfile in "$PID_DIR"/socat.pid "$PID_DIR"/socat_call.pid "$PID_DIR"/recv_loop.pid; do + if [ -f "$pidfile" ]; then + local pid + pid=$(cat "$pidfile" 2>/dev/null) + if [ -n "$pid" ]; then + kill "$pid" 2>/dev/null || true + kill -9 "$pid" 2>/dev/null || true + fi + rm -f "$pidfile" fi - stty sane 2>/dev/null || true - ORIGINAL_STTY="" + done - # Close pipe file descriptors to unblock any blocking reads - # NOTE: must use { } group so 2>/dev/null doesn't permanently redirect stderr - { exec 3<&-; } 2>/dev/null || true - { exec 4>&-; } 2>/dev/null || true - - # Kill all call-related processes by PID files - for pidfile in "$PID_DIR"/socat.pid "$PID_DIR"/socat_call.pid "$PID_DIR"/recv_loop.pid; do - if [ -f "$pidfile" ]; then - local pid - pid=$(cat "$pidfile" 2>/dev/null) - if [ -n "$pid" ]; then - kill "$pid" 2>/dev/null || true - kill -9 "$pid" 2>/dev/null || true - fi - rm -f "$pidfile" - fi - done - - # Kill recording process if active - if [ -n "$REC_PID" ]; then - kill "$REC_PID" 2>/dev/null || true - kill -9 "$REC_PID" 2>/dev/null || true - REC_PID="" - fi - - # Wait briefly for processes to die - sleep 0.2 - - # Kill volume monitor if active - if [ -n "$VOL_MON_PID" ]; then - kill "$VOL_MON_PID" 2>/dev/null || true - kill -9 "$VOL_MON_PID" 2>/dev/null || true - VOL_MON_PID="" - fi - - # Remove all runtime files for this PID - rm -f "$PTT_FLAG" "$CONNECTED_FLAG" - rm -f "$RECV_PIPE" "$SEND_PIPE" - rm -f "$CIPHER_RUNTIME_FILE" - rm -f "$HMAC_RUNTIME_FILE" - rm -f "$NONCE_LOG_FILE" - rm -f "$DATA_DIR/run/remote_id_$$" - rm -f "$DATA_DIR/run/remote_cipher_$$" - rm -f "$DATA_DIR/run/incoming_$$" - rm -f "$DATA_DIR/run/vol_ptt_trigger_$$" - - # Kill circuit refresh if active - if [ -n "$CIRCUIT_REFRESH_PID" ]; then - kill "$CIRCUIT_REFRESH_PID" 2>/dev/null || true - CIRCUIT_REFRESH_PID="" - fi - - # Clean temp audio files - rm -f "$AUDIO_DIR"/*.tmp 2>/dev/null || true - - # Reset state variables - CALL_ACTIVE=0 + # Kill recording process if active + if [ -n "$REC_PID" ]; then + kill "$REC_PID" 2>/dev/null || true + kill -9 "$REC_PID" 2>/dev/null || true REC_PID="" + fi + + # Wait briefly for processes to die + sleep 0.2 + + # Kill volume monitor if active + if [ -n "$VOL_MON_PID" ]; then + kill "$VOL_MON_PID" 2>/dev/null || true + kill -9 "$VOL_MON_PID" 2>/dev/null || true + VOL_MON_PID="" + fi + + # Remove all runtime files for this PID + rm -f "$PTT_FLAG" "$CONNECTED_FLAG" + rm -f "$RECV_PIPE" "$SEND_PIPE" + rm -f "$CIPHER_RUNTIME_FILE" + rm -f "$HMAC_RUNTIME_FILE" + rm -f "$NONCE_LOG_FILE" + rm -f "$DATA_DIR/run/remote_id_$$" + rm -f "$DATA_DIR/run/remote_cipher_$$" + rm -f "$DATA_DIR/run/incoming_$$" + rm -f "$DATA_DIR/run/vol_ptt_trigger_$$" + + # Kill circuit refresh if active + if [ -n "$CIRCUIT_REFRESH_PID" ]; then + kill "$CIRCUIT_REFRESH_PID" 2>/dev/null || true + CIRCUIT_REFRESH_PID="" + fi + + # Clean temp audio files + rm -f "$AUDIO_DIR"/*.tmp 2>/dev/null || true + + # Reset state variables + CALL_ACTIVE=0 + REC_PID="" } #============================================================================= # AUTO-LISTEN (BACKGROUND LISTENER) #============================================================================= start_auto_listener() { - # Only start if auto-listen is enabled and Tor is running - if [ "$AUTO_LISTEN" -ne 1 ]; then return 0; fi - if [ -z "$SHARED_SECRET" ]; then return 0; fi - if [ -z "$TOR_PID" ] || ! kill -0 "$TOR_PID" 2>/dev/null; then return 0; fi + # Only start if auto-listen is enabled and Tor is running + if [ "$AUTO_LISTEN" -ne 1 ]; then return 0; fi + if [ -z "$SHARED_SECRET" ]; then return 0; fi + if [ -z "$TOR_PID" ] || ! kill -0 "$TOR_PID" 2>/dev/null; then return 0; fi - # Stop any existing listener first - stop_auto_listener + # Stop any existing listener first + stop_auto_listener - mkdir -p "$AUDIO_DIR" "$DATA_DIR/run" - rm -f "$RECV_PIPE" "$SEND_PIPE" "$AUTO_LISTEN_FLAG" - mkfifo "$RECV_PIPE" "$SEND_PIPE" + mkdir -p "$AUDIO_DIR" "$DATA_DIR/run" + rm -f "$RECV_PIPE" "$SEND_PIPE" "$AUTO_LISTEN_FLAG" + mkfifo "$RECV_PIPE" "$SEND_PIPE" - socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \ - "SYSTEM:touch $AUTO_LISTEN_FLAG; cat $SEND_PIPE & cat > $RECV_PIPE" & - AUTO_LISTEN_PID=$! - save_pid "socat" "$AUTO_LISTEN_PID" + socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \ + "SYSTEM:touch $AUTO_LISTEN_FLAG; cat $SEND_PIPE & cat > $RECV_PIPE" & + AUTO_LISTEN_PID=$! + save_pid "socat" "$AUTO_LISTEN_PID" } stop_auto_listener() { - if [ -n "$AUTO_LISTEN_PID" ]; then - kill "$AUTO_LISTEN_PID" 2>/dev/null || true - kill -9 "$AUTO_LISTEN_PID" 2>/dev/null || true - AUTO_LISTEN_PID="" - fi - rm -f "$AUTO_LISTEN_FLAG" "$RECV_PIPE" "$SEND_PIPE" + if [ -n "$AUTO_LISTEN_PID" ]; then + kill "$AUTO_LISTEN_PID" 2>/dev/null || true + kill -9 "$AUTO_LISTEN_PID" 2>/dev/null || true + AUTO_LISTEN_PID="" + fi + rm -f "$AUTO_LISTEN_FLAG" "$RECV_PIPE" "$SEND_PIPE" } #============================================================================= @@ -1140,248 +1133,248 @@ stop_auto_listener() { VOL_MON_PID="" start_vol_monitor() { - local trigger_file="${1:-$DATA_DIR/run/vol_ptt_trigger_$$}" - [ "$VOL_PTT" -ne 1 ] && return - [ "$IS_TERMUX" -ne 1 ] && return - if ! check_dep jq; then - log_warn "jq not found — Volume PTT disabled" - return - fi - if ! check_dep termux-volume; then - log_warn "termux-volume not found — Volume PTT disabled" - return - fi + local trigger_file="${1:-$DATA_DIR/run/vol_ptt_trigger_$$}" + [ "$VOL_PTT" -ne 1 ] && return + [ "$IS_TERMUX" -ne 1 ] && return + if ! check_dep jq; then + log_warn "jq not found — Volume PTT disabled" + return + fi + if ! check_dep termux-volume; then + log_warn "termux-volume not found — Volume PTT disabled" + return + fi - rm -f "$trigger_file" + rm -f "$trigger_file" - ( - local last_vol="" - local last_tap=0 - local restore_vol="" + ( + local last_vol="" + local last_tap=0 + local restore_vol="" - while [ -f "$CONNECTED_FLAG" ]; do - local cur_vol - cur_vol=$(termux-volume 2>/dev/null \ - | jq -r '.[] | select(.stream=="music") | .volume' 2>/dev/null \ - || echo "") + while [ -f "$CONNECTED_FLAG" ]; do + local cur_vol + cur_vol=$(termux-volume 2>/dev/null | + jq -r '.[] | select(.stream=="music") | .volume' 2>/dev/null || + echo "") - # Remember the initial volume so we can restore after detection - if [ -n "$cur_vol" ] && [ -z "$restore_vol" ]; then - restore_vol="$cur_vol" + # Remember the initial volume so we can restore after detection + if [ -n "$cur_vol" ] && [ -z "$restore_vol" ]; then + restore_vol="$cur_vol" + fi + + if [ -n "$cur_vol" ] && [ -n "$last_vol" ]; then + local drop=$((last_vol - cur_vol)) + + if [ "$drop" -ge 2 ] 2>/dev/null; then + # Rapid double-tap: both presses landed in one poll cycle + touch "$trigger_file" + last_tap=0 + # Restore volume for next use + if [ -n "$restore_vol" ]; then + termux-volume music "$restore_vol" 2>/dev/null || true + last_vol="$restore_vol" + sleep 0.5 + continue + fi + elif [ "$drop" -ge 1 ] 2>/dev/null; then + # Single press — check if second press follows within 1s + local now + now=$(date +%s) + if [ "$last_tap" -gt 0 ] && [ $((now - last_tap)) -le 1 ]; then + touch "$trigger_file" + last_tap=0 + if [ -n "$restore_vol" ]; then + termux-volume music "$restore_vol" 2>/dev/null || true + last_vol="$restore_vol" + sleep 0.5 + continue fi - - if [ -n "$cur_vol" ] && [ -n "$last_vol" ]; then - local drop=$(( last_vol - cur_vol )) - - if [ "$drop" -ge 2 ] 2>/dev/null; then - # Rapid double-tap: both presses landed in one poll cycle - touch "$trigger_file" - last_tap=0 - # Restore volume for next use - if [ -n "$restore_vol" ]; then - termux-volume music "$restore_vol" 2>/dev/null || true - last_vol="$restore_vol" - sleep 0.5 - continue - fi - elif [ "$drop" -ge 1 ] 2>/dev/null; then - # Single press — check if second press follows within 1s - local now - now=$(date +%s) - if [ "$last_tap" -gt 0 ] && [ $((now - last_tap)) -le 1 ]; then - touch "$trigger_file" - last_tap=0 - if [ -n "$restore_vol" ]; then - termux-volume music "$restore_vol" 2>/dev/null || true - last_vol="$restore_vol" - sleep 0.5 - continue - fi - else - last_tap=$now - fi - fi - fi - last_vol="$cur_vol" - sleep 0.4 - done - ) & - VOL_MON_PID=$! + else + last_tap=$now + fi + fi + fi + last_vol="$cur_vol" + sleep 0.4 + done + ) & + VOL_MON_PID=$! } stop_vol_monitor() { - if [ -n "$VOL_MON_PID" ]; then - kill "$VOL_MON_PID" 2>/dev/null || true - kill -9 "$VOL_MON_PID" 2>/dev/null || true - VOL_MON_PID="" - fi - rm -f "$DATA_DIR/run/vol_ptt_trigger_$$" + if [ -n "$VOL_MON_PID" ]; then + kill "$VOL_MON_PID" 2>/dev/null || true + kill -9 "$VOL_MON_PID" 2>/dev/null || true + VOL_MON_PID="" + fi + rm -f "$DATA_DIR/run/vol_ptt_trigger_$$" } # Check if an incoming call arrived on the background listener check_auto_listen() { - if [ -f "$AUTO_LISTEN_FLAG" ]; then - rm -f "$AUTO_LISTEN_FLAG" - touch "$CONNECTED_FLAG" - echo -e "\n ${GREEN}${BOLD}Incoming call detected!${NC}" >&2 - sleep 0.5 - in_call_session "$RECV_PIPE" "$SEND_PIPE" "" - cleanup_call - # Restart listener for next call - start_auto_listener - return 0 - fi - return 1 + if [ -f "$AUTO_LISTEN_FLAG" ]; then + rm -f "$AUTO_LISTEN_FLAG" + touch "$CONNECTED_FLAG" + echo -e "\n ${GREEN}${BOLD}Incoming call detected!${NC}" >&2 + sleep 0.5 + in_call_session "$RECV_PIPE" "$SEND_PIPE" "" + cleanup_call + # Restart listener for next call + start_auto_listener + return 0 + fi + return 1 } # Start listening for incoming calls (manual, blocking) listen_for_call() { - if [ -z "$SHARED_SECRET" ]; then - log_err "No shared secret set! Use option 4 first." - return 1 + if [ -z "$SHARED_SECRET" ]; then + log_err "No shared secret set! Use option 4 first." + return 1 + fi + + start_tor || return 1 + + # Stop auto-listener if running (we'll do manual listen) + stop_auto_listener + + local onion + onion=$(get_onion) + echo -e "\n${BOLD}${CYAN}═══ Listening for Calls ═══${NC}\n" + echo -e " ${GREEN}Your address:${NC} ${BOLD}${WHITE}$onion${NC}" + echo -e " ${GREEN}Listening on:${NC} port $LISTEN_PORT" + echo -e "\n ${DIM}Share your .onion address with the caller.${NC}" + echo -e " ${DIM}[Q] Stop listening [B] Listen in background${NC}\n" + + mkdir -p "$AUDIO_DIR" + log_info "Waiting for incoming connection..." + + rm -f "$RECV_PIPE" "$SEND_PIPE" + mkfifo "$RECV_PIPE" "$SEND_PIPE" + + local incoming_flag="$DATA_DIR/run/incoming_$$" + rm -f "$incoming_flag" + + socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \ + "SYSTEM:touch $incoming_flag; cat $SEND_PIPE & cat > $RECV_PIPE" & + local socat_pid=$! + save_pid "socat" "$socat_pid" + + while [ ! -f "$incoming_flag" ]; do + if ! kill -0 "$socat_pid" 2>/dev/null; then + log_err "Listener stopped unexpectedly" + rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" + # Restart auto-listener if enabled + start_auto_listener + return 1 fi + # Read user input with 1-second timeout + local user_input="" + if read -r -t 1 user_input 2>/dev/null; then + case "$user_input" in + q | Q) + # Stop all listening (manual + auto), return to menu + kill "$socat_pid" 2>/dev/null || true + wait "$socat_pid" 2>/dev/null || true + rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" + stop_auto_listener + AUTO_LISTEN=0 + save_config + log_info "Stopped listening." + return 0 + ;; + b | B) + # Move to background: kill manual socat, enable auto-listen + kill "$socat_pid" 2>/dev/null || true + wait "$socat_pid" 2>/dev/null || true + rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" + AUTO_LISTEN=1 + save_config + start_auto_listener + log_ok "Listening in background. Returning to menu." + sleep 1 + return 0 + ;; + esac + fi + done - start_tor || return 1 + touch "$CONNECTED_FLAG" + log_ok "Call connected!" + in_call_session "$RECV_PIPE" "$SEND_PIPE" "" + cleanup_call - # Stop auto-listener if running (we'll do manual listen) - stop_auto_listener - - local onion - onion=$(get_onion) - echo -e "\n${BOLD}${CYAN}═══ Listening for Calls ═══${NC}\n" - echo -e " ${GREEN}Your address:${NC} ${BOLD}${WHITE}$onion${NC}" - echo -e " ${GREEN}Listening on:${NC} port $LISTEN_PORT" - echo -e "\n ${DIM}Share your .onion address with the caller.${NC}" - echo -e " ${DIM}[Q] Stop listening [B] Listen in background${NC}\n" - - mkdir -p "$AUDIO_DIR" - log_info "Waiting for incoming connection..." - - rm -f "$RECV_PIPE" "$SEND_PIPE" - mkfifo "$RECV_PIPE" "$SEND_PIPE" - - local incoming_flag="$DATA_DIR/run/incoming_$$" - rm -f "$incoming_flag" - - socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \ - "SYSTEM:touch $incoming_flag; cat $SEND_PIPE & cat > $RECV_PIPE" & - local socat_pid=$! - save_pid "socat" "$socat_pid" - - while [ ! -f "$incoming_flag" ]; do - if ! kill -0 "$socat_pid" 2>/dev/null; then - log_err "Listener stopped unexpectedly" - rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" - # Restart auto-listener if enabled - start_auto_listener - return 1 - fi - # Read user input with 1-second timeout - local user_input="" - if read -r -t 1 user_input 2>/dev/null; then - case "$user_input" in - q|Q) - # Stop all listening (manual + auto), return to menu - kill "$socat_pid" 2>/dev/null || true - wait "$socat_pid" 2>/dev/null || true - rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" - stop_auto_listener - AUTO_LISTEN=0 - save_config - log_info "Stopped listening." - return 0 - ;; - b|B) - # Move to background: kill manual socat, enable auto-listen - kill "$socat_pid" 2>/dev/null || true - wait "$socat_pid" 2>/dev/null || true - rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" - AUTO_LISTEN=1 - save_config - start_auto_listener - log_ok "Listening in background. Returning to menu." - sleep 1 - return 0 - ;; - esac - fi - done - - touch "$CONNECTED_FLAG" - log_ok "Call connected!" - in_call_session "$RECV_PIPE" "$SEND_PIPE" "" - cleanup_call - - # Restart auto-listener if enabled - start_auto_listener + # Restart auto-listener if enabled + start_auto_listener } # Call a remote .onion address call_remote() { - if [ -z "$SHARED_SECRET" ]; then - log_err "No shared secret set! Use option 4 first." - return 1 - fi + if [ -z "$SHARED_SECRET" ]; then + log_err "No shared secret set! Use option 4 first." + return 1 + fi - echo -e "\n${BOLD}${CYAN}═══ Make a Call ═══${NC}\n" - echo -ne " ${BOLD}Enter .onion address: ${NC}" - read -r remote_onion + echo -e "\n${BOLD}${CYAN}═══ Make a Call ═══${NC}\n" + echo -ne " ${BOLD}Enter .onion address: ${NC}" + read -r remote_onion - if [ -z "$remote_onion" ]; then - log_warn "No address entered" - return 1 - fi + if [ -z "$remote_onion" ]; then + log_warn "No address entered" + return 1 + fi - # Append .onion if not present - if [[ "$remote_onion" != *.onion ]]; then - remote_onion="${remote_onion}.onion" - fi + # Append .onion if not present + if [[ "$remote_onion" != *.onion ]]; then + remote_onion="${remote_onion}.onion" + fi - start_tor || return 1 + start_tor || return 1 - echo -e "\n ${DIM}Connecting to ${remote_onion}:${LISTEN_PORT} via Tor...${NC}" + echo -e "\n ${DIM}Connecting to ${remote_onion}:${LISTEN_PORT} via Tor...${NC}" - mkdir -p "$AUDIO_DIR" - touch "$CONNECTED_FLAG" + mkdir -p "$AUDIO_DIR" + touch "$CONNECTED_FLAG" - # Create named pipes - rm -f "$RECV_PIPE" "$SEND_PIPE" - mkfifo "$RECV_PIPE" "$SEND_PIPE" + # Create named pipes + rm -f "$RECV_PIPE" "$SEND_PIPE" + mkfifo "$RECV_PIPE" "$SEND_PIPE" - # Connect via Tor SOCKS proxy using socat - socat "SOCKS4A:127.0.0.1:${remote_onion}:${LISTEN_PORT},socksport=${TOR_SOCKS_PORT}" \ - "SYSTEM:cat $SEND_PIPE & cat > $RECV_PIPE" & - local socat_pid=$! - save_pid "socat_call" "$socat_pid" + # Connect via Tor SOCKS proxy using socat + socat "SOCKS4A:127.0.0.1:${remote_onion}:${LISTEN_PORT},socksport=${TOR_SOCKS_PORT}" \ + "SYSTEM:cat $SEND_PIPE & cat > $RECV_PIPE" & + local socat_pid=$! + save_pid "socat_call" "$socat_pid" - # Animated connecting indicator while socat establishes connection - ( - local dots="" - while true; do - for dots in "." ".." "..." " "; do - echo -ne "\r ${CYAN}${BOLD}Connecting${dots}${NC} " >&2 - sleep 0.3 - done - done - ) & - local spinner_pid=$! + # Animated connecting indicator while socat establishes connection + ( + local dots="" + while true; do + for dots in "." ".." "..." " "; do + echo -ne "\r ${CYAN}${BOLD}Connecting${dots}${NC} " >&2 + sleep 0.3 + done + done + ) & + local spinner_pid=$! - # Give socat a moment to connect - sleep 2 + # Give socat a moment to connect + sleep 2 - if kill -0 "$socat_pid" 2>/dev/null; then - in_call_session "$RECV_PIPE" "$SEND_PIPE" "$remote_onion" "$spinner_pid" - else - # Kill spinner and show error - kill "$spinner_pid" 2>/dev/null || true - wait "$spinner_pid" 2>/dev/null || true - echo -ne "\r " >&2 - echo "" >&2 - log_err "Failed to connect. Check the address and ensure Tor is running." - fi + if kill -0 "$socat_pid" 2>/dev/null; then + in_call_session "$RECV_PIPE" "$SEND_PIPE" "$remote_onion" "$spinner_pid" + else + # Kill spinner and show error + kill "$spinner_pid" 2>/dev/null || true + wait "$spinner_pid" 2>/dev/null || true + echo -ne "\r " >&2 + echo "" >&2 + log_err "Failed to connect. Check the address and ensure Tor is running." + fi - # Full cleanup after call ends - cleanup_call + # Full cleanup after call ends + cleanup_call } #============================================================================= @@ -1390,503 +1383,511 @@ call_remote() { # Draw the call header (reusable for redraw after settings) draw_call_header() { - local _remote="${1:-}" - local _rcipher="${2:-}" - clear >&2 - # Row counter for ANSI cursor positioning (clear sets cursor to row 1) - local _r=1 + local _remote="${1:-}" + local _rcipher="${2:-}" + clear >&2 + # Row counter for ANSI cursor positioning (clear sets cursor to row 1) + local _r=1 - if [ -n "$_remote" ]; then - echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC} ${CYAN}${_remote}${NC}\n" >&2 + if [ -n "$_remote" ]; then + echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC} ${CYAN}${_remote}${NC}\n" >&2 + else + echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC}\n" >&2 + fi + _r=4 # \n(row1) + header(row2) + \n(row3) + echo-newline → cursor at row 4 + + # Cipher info + CIPHER_ROW=$_r + local cipher_upper="${CIPHER^^}" + if [ -n "$_rcipher" ]; then + local rcipher_upper="${_rcipher^^}" + if [ "$_rcipher" = "$CIPHER" ]; then + echo -e " ${GREEN}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 + echo -e " ${GREEN}●${NC} Remote cipher: ${WHITE}${rcipher_upper}${NC}" >&2 else - echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC}\n" >&2 + echo -e " ${RED}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 + echo -e " ${RED}●${NC} Remote cipher: ${WHITE}${rcipher_upper}${NC}" >&2 fi - _r=4 # \n(row1) + header(row2) + \n(row3) + echo-newline → cursor at row 4 + else + echo -e " ${GREEN}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 + echo -e " ${DIM}●${NC} Remote cipher: ${DIM}waiting...${NC}" >&2 + fi + _r=$((_r + 2)) - # Cipher info - CIPHER_ROW=$_r - local cipher_upper="${CIPHER^^}" - if [ -n "$_rcipher" ]; then - local rcipher_upper="${_rcipher^^}" - if [ "$_rcipher" = "$CIPHER" ]; then - echo -e " ${GREEN}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 - echo -e " ${GREEN}●${NC} Remote cipher: ${WHITE}${rcipher_upper}${NC}" >&2 + # Snowflake bridge info + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + local tor_log="$TOR_DIR/tor.log" + echo "" >&2 + _r=$((_r + 1)) + echo -e " ${TOR_PURPLE}●${NC} ${BOLD}Snowflake bridge${NC}" >&2 + _r=$((_r + 1)) + if [ -f "$tor_log" ]; then + local bridge_line="" + bridge_line=$(grep "new bridge descriptor" "$tor_log" 2>/dev/null | tail -1 || true) + if [ -n "$bridge_line" ]; then + local bridge_name="" + bridge_name=$(echo "$bridge_line" | sed -n "s/.*new bridge descriptor '\([^']*\)'.*/\1/p" || true) + local bridge_fp="" + bridge_fp=$(echo "$bridge_line" | sed -n 's/.*(\(fresh\|stale\)): \(.*\)/\2/p' || true) + if [ -n "$bridge_name" ]; then + local fp_display="$bridge_fp" + if [ ${#fp_display} -gt 40 ]; then + fp_display="${fp_display:0:40}..." + fi + echo -e " ${DIM}descriptor:${NC} ${WHITE}${bridge_name}${NC} ${DIM}— ${fp_display}${NC}" >&2 + _r=$((_r + 1)) + fi + fi + local proxy_line="" + proxy_line=$(grep 'Managed proxy.*snowflake' "$tor_log" 2>/dev/null | tail -1 || true) + if [ -n "$proxy_line" ]; then + if echo "$proxy_line" | grep -q "connected"; then + echo -e " ${DIM}transport:${NC} ${GREEN}connected${NC}" >&2 else - echo -e " ${RED}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 - echo -e " ${RED}●${NC} Remote cipher: ${WHITE}${rcipher_upper}${NC}" >&2 + echo -e " ${DIM}transport:${NC} ${YELLOW}connecting...${NC}" >&2 fi - else - echo -e " ${GREEN}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 - echo -e " ${DIM}●${NC} Remote cipher: ${DIM}waiting...${NC}" >&2 + _r=$((_r + 1)) + fi fi - _r=$((_r + 2)) + fi - # Snowflake bridge info - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - local tor_log="$TOR_DIR/tor.log" - echo "" >&2; _r=$((_r + 1)) - echo -e " ${TOR_PURPLE}●${NC} ${BOLD}Snowflake bridge${NC}" >&2; _r=$((_r + 1)) - if [ -f "$tor_log" ]; then - local bridge_line="" - bridge_line=$(grep "new bridge descriptor" "$tor_log" 2>/dev/null | tail -1 || true) - if [ -n "$bridge_line" ]; then - local bridge_name="" - bridge_name=$(echo "$bridge_line" | sed -n "s/.*new bridge descriptor '\([^']*\)'.*/\1/p" || true) - local bridge_fp="" - bridge_fp=$(echo "$bridge_line" | sed -n 's/.*(\(fresh\|stale\)): \(.*\)/\2/p' || true) - if [ -n "$bridge_name" ]; then - local fp_display="$bridge_fp" - if [ ${#fp_display} -gt 40 ]; then - fp_display="${fp_display:0:40}..." - fi - echo -e " ${DIM}descriptor:${NC} ${WHITE}${bridge_name}${NC} ${DIM}— ${fp_display}${NC}" >&2 - _r=$((_r + 1)) - fi - fi - local proxy_line="" - proxy_line=$(grep 'Managed proxy.*snowflake' "$tor_log" 2>/dev/null | tail -1 || true) - if [ -n "$proxy_line" ]; then - if echo "$proxy_line" | grep -q "connected"; then - echo -e " ${DIM}transport:${NC} ${GREEN}connected${NC}" >&2 - else - echo -e " ${DIM}transport:${NC} ${YELLOW}connecting...${NC}" >&2 - fi - _r=$((_r + 1)) - fi - fi + # Circuit hop display (vertical) + CIRCUIT_START_ROW=0 + CIRCUIT_HOP_COUNT=0 + if [ "$SHOW_CIRCUIT" -eq 1 ]; then + local _hop_data + _hop_data=$(get_circuit_hops 2>/dev/null) || true + if [ -n "$_hop_data" ]; then + echo "" >&2 + _r=$((_r + 1)) + echo -e " ${TOR_PURPLE}●${NC} ${BOLD}Circuit${NC}" >&2 + _r=$((_r + 1)) + CIRCUIT_START_ROW=$_r + local _hop_i=0 _hop_total + _hop_total=$(echo "$_hop_data" | wc -l) + while IFS='|' read -r _hname _hcountry; do + _hop_i=$((_hop_i + 1)) + local _hlabel="Relay" + [ $_hop_i -eq 1 ] && _hlabel="Guard" + [ $_hop_i -eq $_hop_total ] && _hlabel="Rendezvous" + printf ' \033[2m%-13s\033[0m \033[1;37m%s\033[0m \033[2m(%s)\033[0m\n' "${_hlabel}:" "$_hname" "$_hcountry" >&2 + _r=$((_r + 1)) + done <<<"$_hop_data" + CIRCUIT_HOP_COUNT=$_hop_i fi + fi - # Circuit hop display (vertical) - CIRCUIT_START_ROW=0 - CIRCUIT_HOP_COUNT=0 - if [ "$SHOW_CIRCUIT" -eq 1 ]; then - local _hop_data - _hop_data=$(get_circuit_hops 2>/dev/null) || true - if [ -n "$_hop_data" ]; then - echo "" >&2; _r=$((_r + 1)) - echo -e " ${TOR_PURPLE}●${NC} ${BOLD}Circuit${NC}" >&2; _r=$((_r + 1)) - CIRCUIT_START_ROW=$_r - local _hop_i=0 _hop_total - _hop_total=$(echo "$_hop_data" | wc -l) - while IFS='|' read -r _hname _hcountry; do - _hop_i=$((_hop_i + 1)) - local _hlabel="Relay" - [ $_hop_i -eq 1 ] && _hlabel="Guard" - [ $_hop_i -eq $_hop_total ] && _hlabel="Rendezvous" - printf ' \033[2m%-13s\033[0m \033[1;37m%s\033[0m \033[2m(%s)\033[0m\n' "${_hlabel}:" "$_hname" "$_hcountry" >&2 - _r=$((_r + 1)) - done <<< "$_hop_data" - CIRCUIT_HOP_COUNT=$_hop_i - fi - fi + echo "" >&2 + _r=$((_r + 1)) - echo "" >&2; _r=$((_r + 1)) + # Static placeholders — updated in-place via ANSI positioning + echo -e " ${DIM}Last sent: --${NC}" >&2 + SENT_INFO_ROW=$_r + _r=$((_r + 1)) - # Static placeholders — updated in-place via ANSI positioning - echo -e " ${DIM}Last sent: --${NC}" >&2 - SENT_INFO_ROW=$_r - _r=$((_r + 1)) + echo -e " ${DIM}Last recv: --${NC}" >&2 + RECV_INFO_ROW=$_r + _r=$((_r + 1)) - echo -e " ${DIM}Last recv: --${NC}" >&2 - RECV_INFO_ROW=$_r - _r=$((_r + 1)) + echo "" >&2 + _r=$((_r + 1)) - echo "" >&2; _r=$((_r + 1)) + # Static status bar + if [ $IS_TERMUX -eq 1 ]; then + echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 + else + echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 + fi + STATUS_ROW=$_r - # Static status bar - if [ $IS_TERMUX -eq 1 ]; then - echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 - else - echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 - fi - STATUS_ROW=$_r - - echo "" >&2 - echo "" >&2 + echo "" >&2 + echo "" >&2 } # Refresh circuit hops in-place during a call (called from background loop) refresh_circuit_display() { - [ "$SHOW_CIRCUIT" -eq 0 ] && return - [ "$CIRCUIT_START_ROW" -eq 0 ] && return - [ "$CIRCUIT_HOP_COUNT" -eq 0 ] && return + [ "$SHOW_CIRCUIT" -eq 0 ] && return + [ "$CIRCUIT_START_ROW" -eq 0 ] && return + [ "$CIRCUIT_HOP_COUNT" -eq 0 ] && return - local _hop_data - _hop_data=$(get_circuit_hops 2>/dev/null) || return - [ -z "$_hop_data" ] && return + local _hop_data + _hop_data=$(get_circuit_hops 2>/dev/null) || return + [ -z "$_hop_data" ] && return - local _hop_i=0 _hop_total - _hop_total=$(echo "$_hop_data" | wc -l) + local _hop_i=0 _hop_total + _hop_total=$(echo "$_hop_data" | wc -l) - printf '\033[s' >&2 # save cursor - while IFS='|' read -r _hname _hcountry; do - _hop_i=$((_hop_i + 1)) - [ $_hop_i -gt $CIRCUIT_HOP_COUNT ] && break # don't overflow allocated rows - local _hlabel="Relay" - [ $_hop_i -eq 1 ] && _hlabel="Guard" - [ $_hop_i -eq $_hop_total ] && _hlabel="Rendezvous" - local _row=$((CIRCUIT_START_ROW + _hop_i - 1)) - printf '\033[%d;1H\033[K' "$_row" >&2 - printf ' \033[2m%-13s\033[0m \033[1;37m%s\033[0m \033[2m(%s)\033[0m' "${_hlabel}:" "$_hname" "$_hcountry" >&2 - done <<< "$_hop_data" - printf '\033[u' >&2 # restore cursor + printf '\033[s' >&2 # save cursor + while IFS='|' read -r _hname _hcountry; do + _hop_i=$((_hop_i + 1)) + [ $_hop_i -gt $CIRCUIT_HOP_COUNT ] && break # don't overflow allocated rows + local _hlabel="Relay" + [ $_hop_i -eq 1 ] && _hlabel="Guard" + [ $_hop_i -eq $_hop_total ] && _hlabel="Rendezvous" + local _row=$((CIRCUIT_START_ROW + _hop_i - 1)) + printf '\033[%d;1H\033[K' "$_row" >&2 + printf ' \033[2m%-13s\033[0m \033[1;37m%s\033[0m \033[2m(%s)\033[0m' "${_hlabel}:" "$_hname" "$_hcountry" >&2 + done <<<"$_hop_data" + printf '\033[u' >&2 # restore cursor } # Background circuit refresh loop (60-second interval) start_circuit_refresh() { - [ "$SHOW_CIRCUIT" -eq 0 ] && return - [ "$CIRCUIT_START_ROW" -eq 0 ] && return - ( - while [ -f "$CONNECTED_FLAG" ]; do - sleep 60 - [ -f "$CONNECTED_FLAG" ] || break - refresh_circuit_display - done - ) & - CIRCUIT_REFRESH_PID=$! + [ "$SHOW_CIRCUIT" -eq 0 ] && return + [ "$CIRCUIT_START_ROW" -eq 0 ] && return + ( + while [ -f "$CONNECTED_FLAG" ]; do + sleep 60 + [ -f "$CONNECTED_FLAG" ] || break + refresh_circuit_display + done + ) & + CIRCUIT_REFRESH_PID=$! } in_call_session() { - local recv_pipe="$1" - local send_pipe="$2" - local known_remote="${3:-}" - local spinner_pid="${4:-}" + local recv_pipe="$1" + local send_pipe="$2" + local known_remote="${3:-}" + local spinner_pid="${4:-}" - CALL_ACTIVE=1 - rm -f "$PTT_FLAG" - mkdir -p "$AUDIO_DIR" + CALL_ACTIVE=1 + rm -f "$PTT_FLAG" + mkdir -p "$AUDIO_DIR" - # Start volume-down double-tap monitor (Termux only) - VOL_MON_PID="" - local vol_trigger_file="$DATA_DIR/run/vol_ptt_trigger_$$" - rm -f "$vol_trigger_file" - start_vol_monitor "$vol_trigger_file" + # Start volume-down double-tap monitor (Termux only) + VOL_MON_PID="" + local vol_trigger_file="$DATA_DIR/run/vol_ptt_trigger_$$" + rm -f "$vol_trigger_file" + start_vol_monitor "$vol_trigger_file" - # Write cipher to runtime file so subshells can track changes - echo "$CIPHER" > "$CIPHER_RUNTIME_FILE" - echo "$HMAC_AUTH" > "$HMAC_RUNTIME_FILE" - : > "$NONCE_LOG_FILE" + # Write cipher to runtime file so subshells can track changes + echo "$CIPHER" >"$CIPHER_RUNTIME_FILE" + echo "$HMAC_AUTH" >"$HMAC_RUNTIME_FILE" + : >"$NONCE_LOG_FILE" - # Open persistent file descriptors for the pipes - exec 3< "$recv_pipe" # fd 3 = read from remote - exec 4> "$send_pipe" # fd 4 = write to remote + # Open persistent file descriptors for the pipes + exec 3<"$recv_pipe" # fd 3 = read from remote + exec 4>"$send_pipe" # fd 4 = write to remote - # Send our onion address and cipher for handshake - local my_onion - my_onion=$(get_onion) - if [ -n "$my_onion" ]; then - proto_send "ID:${my_onion}" + # Send our onion address and cipher for handshake + local my_onion + my_onion=$(get_onion) + if [ -n "$my_onion" ]; then + proto_send "ID:${my_onion}" + fi + proto_send "CIPHER:${CIPHER}" + + # Remote address and cipher (populated by handshake / receive loop) + local remote_id_file="$DATA_DIR/run/remote_id_$$" + local remote_cipher_file="$DATA_DIR/run/remote_cipher_$$" + rm -f "$remote_id_file" "$remote_cipher_file" + + # If we don't know the remote address yet (listener), wait briefly for handshake + local remote_display="$known_remote" + local remote_cipher="" + if [ -z "$remote_display" ]; then + # Read first line — should be ID + local first_line="" + if read -r -t 3 first_line <&3 2>/dev/null; then + first_line=$(proto_verify "$first_line") || first_line="" + if [[ "$first_line" == ID:* ]]; then + remote_display="${first_line#ID:}" + echo "$remote_display" >"$remote_id_file" + elif [[ "$first_line" == CIPHER:* ]]; then + remote_cipher="${first_line#CIPHER:}" + fi fi - proto_send "CIPHER:${CIPHER}" + fi - # Remote address and cipher (populated by handshake / receive loop) - local remote_id_file="$DATA_DIR/run/remote_id_$$" - local remote_cipher_file="$DATA_DIR/run/remote_cipher_$$" - rm -f "$remote_id_file" "$remote_cipher_file" - - # If we don't know the remote address yet (listener), wait briefly for handshake - local remote_display="$known_remote" - local remote_cipher="" - if [ -z "$remote_display" ]; then - # Read first line — should be ID - local first_line="" - if read -r -t 3 first_line <&3 2>/dev/null; then - first_line=$(proto_verify "$first_line") || first_line="" - if [[ "$first_line" == ID:* ]]; then - remote_display="${first_line#ID:}" - echo "$remote_display" > "$remote_id_file" - elif [[ "$first_line" == CIPHER:* ]]; then - remote_cipher="${first_line#CIPHER:}" - fi - fi + # Try to read CIPHER: line (quick, non-blocking) + if [ -z "$remote_cipher" ]; then + local cline="" + if read -r -t 1 cline <&3 2>/dev/null; then + cline=$(proto_verify "$cline") || cline="" + if [[ "$cline" == CIPHER:* ]]; then + remote_cipher="${cline#CIPHER:}" + fi fi + fi - # Try to read CIPHER: line (quick, non-blocking) - if [ -z "$remote_cipher" ]; then - local cline="" - if read -r -t 1 cline <&3 2>/dev/null; then - cline=$(proto_verify "$cline") || cline="" - if [[ "$cline" == CIPHER:* ]]; then - remote_cipher="${cline#CIPHER:}" - fi - fi - fi + # Save remote cipher for later redraws + if [ -n "$remote_cipher" ]; then + echo "$remote_cipher" >"$remote_cipher_file" + fi - # Save remote cipher for later redraws - if [ -n "$remote_cipher" ]; then - echo "$remote_cipher" > "$remote_cipher_file" - fi + # Kill connecting spinner and draw call header + if [ -n "$spinner_pid" ]; then + kill "$spinner_pid" 2>/dev/null || true + wait "$spinner_pid" 2>/dev/null || true + fi + draw_call_header "$remote_display" "$remote_cipher" - # Kill connecting spinner and draw call header - if [ -n "$spinner_pid" ]; then - kill "$spinner_pid" 2>/dev/null || true - wait "$spinner_pid" 2>/dev/null || true - fi - draw_call_header "$remote_display" "$remote_cipher" - - # Start periodic circuit refresh - CIRCUIT_REFRESH_PID="" - start_circuit_refresh - - # Start receive handler in background - # Protocol: ID:, PTT_START, PTT_STOP, PING, - # or "AUDIO:" - ( - while [ -f "$CONNECTED_FLAG" ]; do - local line="" - if read -r line <&3 2>/dev/null; then - line=$(proto_verify "$line") || continue - case "$line" in - PTT_START) - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - printf ' \033[42;1;37m REMOTE TALKING \033[0m ' >&2 - printf '\033[u' >&2 - ;; - PTT_STOP) - # silent — Last recv row provides feedback - ;; - PING) - # silent — no display update - ;; - ID:*) - # Caller ID received (save but don't print — already in header) - local remote_addr="${line#ID:}" - echo "$remote_addr" > "$remote_id_file" - ;; - CIPHER:*) - # Remote side sent/changed their cipher — save and update display - local rc="${line#CIPHER:}" - echo "$rc" > "$remote_cipher_file" 2>/dev/null || true - # Read current local cipher from runtime file - local _cur_cipher="$CIPHER" - [ -f "$CIPHER_RUNTIME_FILE" ] && _cur_cipher=$(cat "$CIPHER_RUNTIME_FILE") - local _cu="${_cur_cipher^^}" - local _ru="${rc^^}" - # Update cipher lines in-place using ANSI cursor positioning (rows 4-5) - local _dot_color - if [ "$rc" = "$_cur_cipher" ]; then - _dot_color="$GREEN" - else - _dot_color="$RED" - fi - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$CIPHER_ROW" >&2 - printf ' %b●%b Local cipher: %b%s%b\r\n' "$_dot_color" "$NC" "$WHITE" "$_cu" "$NC" >&2 - printf '\033[K' >&2 - printf ' %b●%b Remote cipher: %b%s%b' "$_dot_color" "$NC" "$WHITE" "$_ru" "$NC" >&2 - printf '\033[u' >&2 - ;; - MSG:*) - # Encrypted text message received - local msg_b64="${line#MSG:}" - local _mid=$(uid) - local msg_enc="$AUDIO_DIR/msg_enc_${_mid}.tmp" - local msg_dec="$AUDIO_DIR/msg_dec_${_mid}.tmp" - echo "$msg_b64" | base64 -d > "$msg_enc" 2>/dev/null || true - if [ -s "$msg_enc" ]; then - if decrypt_file "$msg_enc" "$msg_dec" 2>/dev/null; then - local msg_text - msg_text=$(cat "$msg_dec" 2>/dev/null) - echo -e "\n ${MAGENTA}${BOLD}[MSG]${NC} ${WHITE}${msg_text}${NC}" >&2 - fi - fi - rm -f "$msg_enc" "$msg_dec" 2>/dev/null - ;; - AUDIO:*) - # Extract base64 data, decode, decrypt, play - local b64_data="${line#AUDIO:}" - local _rid=$(uid) - local enc_file="$AUDIO_DIR/recv_enc_${_rid}.tmp" - local dec_file="$AUDIO_DIR/recv_dec_${_rid}.tmp" - - echo "$b64_data" | base64 -d > "$enc_file" 2>/dev/null || true - if [ -s "$enc_file" ]; then - if decrypt_file "$enc_file" "$dec_file" 2>/dev/null; then - # Calculate recv size - local _enc_sz=0 - _enc_sz=$(stat -c%s "$enc_file" 2>/dev/null || echo 0) - local _sz_kb=$(( _enc_sz * 10 / 1024 )) - local _sz_w=$(( _sz_kb / 10 )) - local _sz_f=$(( _sz_kb % 10 )) - local _recv_info="${_sz_w}.${_sz_f}KB" - # Update static "Last recv" row via ANSI positioning - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$RECV_INFO_ROW" >&2 - printf ' \033[2mLast recv: \033[0m\033[1;37m%s\033[0m' "$_recv_info" >&2 - printf '\033[u' >&2 - - play_chunk "$dec_file" 2>/dev/null || true - fi - fi - rm -f "$enc_file" "$dec_file" 2>/dev/null - ;; - HANGUP) - # Remote party hung up - echo -e "\r\n\r\n ${YELLOW}${BOLD}Remote party hung up.${NC}" >&2 - rm -f "$CONNECTED_FLAG" - break - ;; - esac - else - # Pipe closed or error — connection lost - echo -e "\r\n\r\n ${RED}${BOLD}Connection lost.${NC}" >&2 - rm -f "$CONNECTED_FLAG" - break - fi - done - ) & - local recv_pid=$! - save_pid "recv_loop" "$recv_pid" - - # Main PTT input loop - ORIGINAL_STTY=$(stty -g) - stty raw -echo -icanon min 0 time 1 - - REC_PID="" - REC_FILE="" - LAST_SENT_INFO="" - local ptt_active=0 - - # Status bar is already drawn by draw_call_header + # Start periodic circuit refresh + CIRCUIT_REFRESH_PID="" + start_circuit_refresh + # Start receive handler in background + # Protocol: ID:, PTT_START, PTT_STOP, PING, + # or "AUDIO:" + ( while [ -f "$CONNECTED_FLAG" ]; do - local key="" - # Check volume-down double-tap trigger - if [ -f "$vol_trigger_file" ]; then - rm -f "$vol_trigger_file" - key="$PTT_KEY" # simulate PTT key press - else - key=$(dd bs=1 count=1 2>/dev/null) || true - fi + local line="" + if read -r line <&3 2>/dev/null; then + line=$(proto_verify "$line") || continue + case "$line" in + PTT_START) + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + printf ' \033[42;1;37m REMOTE TALKING \033[0m ' >&2 + printf '\033[u' >&2 + ;; + PTT_STOP) + # silent — Last recv row provides feedback + ;; + PING) + # silent — no display update + ;; + ID:*) + # Caller ID received (save but don't print — already in header) + local remote_addr="${line#ID:}" + echo "$remote_addr" >"$remote_id_file" + ;; + CIPHER:*) + # Remote side sent/changed their cipher — save and update display + local rc="${line#CIPHER:}" + echo "$rc" >"$remote_cipher_file" 2>/dev/null || true + # Read current local cipher from runtime file + local _cur_cipher="$CIPHER" + [ -f "$CIPHER_RUNTIME_FILE" ] && _cur_cipher=$(cat "$CIPHER_RUNTIME_FILE") + local _cu="${_cur_cipher^^}" + local _ru="${rc^^}" + # Update cipher lines in-place using ANSI cursor positioning (rows 4-5) + local _dot_color + if [ "$rc" = "$_cur_cipher" ]; then + _dot_color="$GREEN" + else + _dot_color="$RED" + fi + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$CIPHER_ROW" >&2 + printf ' %b●%b Local cipher: %b%s%b\r\n' "$_dot_color" "$NC" "$WHITE" "$_cu" "$NC" >&2 + printf '\033[K' >&2 + printf ' %b●%b Remote cipher: %b%s%b' "$_dot_color" "$NC" "$WHITE" "$_ru" "$NC" >&2 + printf '\033[u' >&2 + ;; + MSG:*) + # Encrypted text message received + local msg_b64="${line#MSG:}" + local _mid=$(uid) + local msg_enc="$AUDIO_DIR/msg_enc_${_mid}.tmp" + local msg_dec="$AUDIO_DIR/msg_dec_${_mid}.tmp" + echo "$msg_b64" | base64 -d >"$msg_enc" 2>/dev/null || true + if [ -s "$msg_enc" ]; then + if decrypt_file "$msg_enc" "$msg_dec" 2>/dev/null; then + local msg_text + msg_text=$(cat "$msg_dec" 2>/dev/null) + echo -e "\n ${MAGENTA}${BOLD}[MSG]${NC} ${WHITE}${msg_text}${NC}" >&2 + fi + fi + rm -f "$msg_enc" "$msg_dec" 2>/dev/null + ;; + AUDIO:*) + # Extract base64 data, decode, decrypt, play + local b64_data="${line#AUDIO:}" + local _rid=$(uid) + local enc_file="$AUDIO_DIR/recv_enc_${_rid}.tmp" + local dec_file="$AUDIO_DIR/recv_dec_${_rid}.tmp" - if [ "$key" = "$PTT_KEY" ]; then - if [ $IS_TERMUX -eq 1 ]; then - # TERMUX: Toggle mode - if [ $ptt_active -eq 0 ]; then - ptt_active=1 - printf '\033[s' >&2; printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - printf ' \033[41;1;37m \u25cf RECORDING \033[0m \033[2m[SPACE]=Send\033[0m ' >&2 - printf '\033[u' >&2 - start_recording - else - ptt_active=0 - stop_and_send - proto_send "PTT_STOP" - # Update Last sent + status bar - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2 - printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2 - printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 - printf '\033[u' >&2 - fi - else - # LINUX: Hold-to-talk - if [ $ptt_active -eq 0 ]; then - ptt_active=1 - printf '\033[s' >&2; printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - printf ' \033[41;1;37;5m \u25cf RECORDING \033[0m ' >&2 - printf '\033[u' >&2 - stty time 5 # longer timeout to span keyboard repeat delay - start_recording - fi - fi + echo "$b64_data" | base64 -d >"$enc_file" 2>/dev/null || true + if [ -s "$enc_file" ]; then + if decrypt_file "$enc_file" "$dec_file" 2>/dev/null; then + # Calculate recv size + local _enc_sz=0 + _enc_sz=$(stat -c%s "$enc_file" 2>/dev/null || echo 0) + local _sz_kb=$((_enc_sz * 10 / 1024)) + local _sz_w=$((_sz_kb / 10)) + local _sz_f=$((_sz_kb % 10)) + local _recv_info="${_sz_w}.${_sz_f}KB" + # Update static "Last recv" row via ANSI positioning + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$RECV_INFO_ROW" >&2 + printf ' \033[2mLast recv: \033[0m\033[1;37m%s\033[0m' "$_recv_info" >&2 + printf '\033[u' >&2 - elif [ "$key" = "q" ] || [ "$key" = "Q" ]; then - # If recording, cancel it - if [ $ptt_active -eq 1 ] && [ -n "$REC_PID" ]; then - if [ $IS_TERMUX -eq 1 ]; then - termux-microphone-record -q &>/dev/null || true - fi - kill "$REC_PID" 2>/dev/null || true - wait "$REC_PID" 2>/dev/null || true - rm -f "$REC_FILE" 2>/dev/null - REC_PID="" - REC_FILE="" + play_chunk "$dec_file" 2>/dev/null || true fi - echo -e "\r\n${YELLOW}Hanging up...${NC}" >&2 - proto_send "HANGUP" - rm -f "$PTT_FLAG" "$CONNECTED_FLAG" - break - - elif [ -z "$key" ]; then - # No key pressed (timeout) — on Linux, release = stop and send - if [ $IS_TERMUX -eq 0 ] && [ $ptt_active -eq 1 ]; then - stty time 1 # restore fast timeout for key detection - ptt_active=0 - stop_and_send - proto_send "PTT_STOP" - # Update Last sent + status bar - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2 - printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2 - printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 - printf '\033[u' >&2 - fi - - elif [ "$key" = "t" ] || [ "$key" = "T" ]; then - # Text chat mode - # Switch to cooked mode for text input - stty "$ORIGINAL_STTY" 2>/dev/null || stty sane - echo "" >&2 - echo -ne " ${CYAN}${BOLD}MSG>${NC} " >&2 - local chat_msg="" - read -r chat_msg - if [ -n "$chat_msg" ]; then - # Encrypt and send - local _cid=$(uid) - local chat_plain="$AUDIO_DIR/chat_${_cid}.tmp" - local chat_enc="$AUDIO_DIR/chat_enc_${_cid}.tmp" - echo -n "$chat_msg" > "$chat_plain" - encrypt_file "$chat_plain" "$chat_enc" 2>/dev/null - if [ -s "$chat_enc" ]; then - local chat_b64 - chat_b64=$(base64 -w 0 "$chat_enc" 2>/dev/null) - proto_send "MSG:${chat_b64}" - echo -e " ${DIM}[you] ${chat_msg}${NC}" >&2 - fi - rm -f "$chat_plain" "$chat_enc" 2>/dev/null - fi - # Switch back to raw mode for PTT - stty raw -echo -icanon min 0 time 1 - # Restore status bar to Ready - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - if [ $IS_TERMUX -eq 1 ]; then - printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 - else - printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 - fi - printf '\033[u' >&2 - - elif [ "$key" = "s" ] || [ "$key" = "S" ]; then - # Mid-call settings - stty "$ORIGINAL_STTY" 2>/dev/null || stty sane - # Flush any leftover raw mode input - read -r -t 0.1 -n 10000 2>/dev/null || true - settings_menu - # Redraw call header and switch back to raw mode - local _rd="" _rc="" - [ -f "$remote_id_file" ] && _rd=$(cat "$remote_id_file" 2>/dev/null) - [ -z "$_rd" ] && _rd="$known_remote" - [ -f "$remote_cipher_file" ] && _rc=$(cat "$remote_cipher_file" 2>/dev/null) - draw_call_header "$_rd" "$_rc" - stty raw -echo -icanon min 0 time 1 - # Restore status bar to Ready (header was redrawn, STATUS_ROW is fresh) - printf '\033[s' >&2 - printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 - if [ $IS_TERMUX -eq 1 ]; then - printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 - else - printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 - fi - printf '\033[u' >&2 - fi + fi + rm -f "$enc_file" "$dec_file" 2>/dev/null + ;; + HANGUP) + # Remote party hung up + echo -e "\r\n\r\n ${YELLOW}${BOLD}Remote party hung up.${NC}" >&2 + rm -f "$CONNECTED_FLAG" + break + ;; + esac + else + # Pipe closed or error — connection lost + echo -e "\r\n\r\n ${RED}${BOLD}Connection lost.${NC}" >&2 + rm -f "$CONNECTED_FLAG" + break + fi done + ) & + local recv_pid=$! + save_pid "recv_loop" "$recv_pid" - echo -e "\n${BOLD}${RED} CALL ENDED ${NC}\n" + # Main PTT input loop + ORIGINAL_STTY=$(stty -g) + stty raw -echo -icanon min 0 time 1 + + REC_PID="" + REC_FILE="" + LAST_SENT_INFO="" + local ptt_active=0 + + # Status bar is already drawn by draw_call_header + + while [ -f "$CONNECTED_FLAG" ]; do + local key="" + # Check volume-down double-tap trigger + if [ -f "$vol_trigger_file" ]; then + rm -f "$vol_trigger_file" + key="$PTT_KEY" # simulate PTT key press + else + key=$(dd bs=1 count=1 2>/dev/null) || true + fi + + if [ "$key" = "$PTT_KEY" ]; then + if [ $IS_TERMUX -eq 1 ]; then + # TERMUX: Toggle mode + if [ $ptt_active -eq 0 ]; then + ptt_active=1 + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + printf ' \033[41;1;37m \u25cf RECORDING \033[0m \033[2m[SPACE]=Send\033[0m ' >&2 + printf '\033[u' >&2 + start_recording + else + ptt_active=0 + stop_and_send + proto_send "PTT_STOP" + # Update Last sent + status bar + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2 + printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 + printf '\033[u' >&2 + fi + else + # LINUX: Hold-to-talk + if [ $ptt_active -eq 0 ]; then + ptt_active=1 + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + printf ' \033[41;1;37;5m \u25cf RECORDING \033[0m ' >&2 + printf '\033[u' >&2 + stty time 5 # longer timeout to span keyboard repeat delay + start_recording + fi + fi + + elif [ "$key" = "q" ] || [ "$key" = "Q" ]; then + # If recording, cancel it + if [ $ptt_active -eq 1 ] && [ -n "$REC_PID" ]; then + if [ $IS_TERMUX -eq 1 ]; then + termux-microphone-record -q &>/dev/null || true + fi + kill "$REC_PID" 2>/dev/null || true + wait "$REC_PID" 2>/dev/null || true + rm -f "$REC_FILE" 2>/dev/null + REC_PID="" + REC_FILE="" + fi + echo -e "\r\n${YELLOW}Hanging up...${NC}" >&2 + proto_send "HANGUP" + rm -f "$PTT_FLAG" "$CONNECTED_FLAG" + break + + elif [ -z "$key" ]; then + # No key pressed (timeout) — on Linux, release = stop and send + if [ $IS_TERMUX -eq 0 ] && [ $ptt_active -eq 1 ]; then + stty time 1 # restore fast timeout for key detection + ptt_active=0 + stop_and_send + proto_send "PTT_STOP" + # Update Last sent + status bar + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2 + printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 + printf '\033[u' >&2 + fi + + elif [ "$key" = "t" ] || [ "$key" = "T" ]; then + # Text chat mode + # Switch to cooked mode for text input + stty "$ORIGINAL_STTY" 2>/dev/null || stty sane + echo "" >&2 + echo -ne " ${CYAN}${BOLD}MSG>${NC} " >&2 + local chat_msg="" + read -r chat_msg + if [ -n "$chat_msg" ]; then + # Encrypt and send + local _cid=$(uid) + local chat_plain="$AUDIO_DIR/chat_${_cid}.tmp" + local chat_enc="$AUDIO_DIR/chat_enc_${_cid}.tmp" + echo -n "$chat_msg" >"$chat_plain" + encrypt_file "$chat_plain" "$chat_enc" 2>/dev/null + if [ -s "$chat_enc" ]; then + local chat_b64 + chat_b64=$(base64 -w 0 "$chat_enc" 2>/dev/null) + proto_send "MSG:${chat_b64}" + echo -e " ${DIM}[you] ${chat_msg}${NC}" >&2 + fi + rm -f "$chat_plain" "$chat_enc" 2>/dev/null + fi + # Switch back to raw mode for PTT + stty raw -echo -icanon min 0 time 1 + # Restore status bar to Ready + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + if [ $IS_TERMUX -eq 1 ]; then + printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 + else + printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 + fi + printf '\033[u' >&2 + + elif [ "$key" = "s" ] || [ "$key" = "S" ]; then + # Mid-call settings + stty "$ORIGINAL_STTY" 2>/dev/null || stty sane + # Flush any leftover raw mode input + read -r -t 0.1 -n 10000 2>/dev/null || true + settings_menu + # Redraw call header and switch back to raw mode + local _rd="" _rc="" + [ -f "$remote_id_file" ] && _rd=$(cat "$remote_id_file" 2>/dev/null) + [ -z "$_rd" ] && _rd="$known_remote" + [ -f "$remote_cipher_file" ] && _rc=$(cat "$remote_cipher_file" 2>/dev/null) + draw_call_header "$_rd" "$_rc" + stty raw -echo -icanon min 0 time 1 + # Restore status bar to Ready (header was redrawn, STATUS_ROW is fresh) + printf '\033[s' >&2 + printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2 + if [ $IS_TERMUX -eq 1 ]; then + printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 + else + printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2 + fi + printf '\033[u' >&2 + fi + done + + echo -e "\n${BOLD}${RED} CALL ENDED ${NC}\n" } #============================================================================= @@ -1894,92 +1895,92 @@ in_call_session() { #============================================================================= test_audio() { - echo -e "\n${BOLD}${CYAN}═══ Audio Loopback Test ═══${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Audio Loopback Test ═══${NC}\n" - # Check dependencies first - local missing=0 - local audio_deps=(opusenc opusdec) - if [ $IS_TERMUX -eq 1 ]; then - audio_deps+=(termux-microphone-record ffmpeg) + # Check dependencies first + local missing=0 + local audio_deps=(opusenc opusdec) + if [ $IS_TERMUX -eq 1 ]; then + audio_deps+=(termux-microphone-record ffmpeg) + else + audio_deps+=(arecord aplay) + fi + for dep in "${audio_deps[@]}"; do + if ! check_dep "$dep"; then + log_err "$dep not found — run option 7 to install dependencies first" + missing=1 + fi + done + if [ $missing -eq 1 ]; then + return 1 + fi + + echo -e " ${DIM}This will record 3 seconds of audio, encode it with Opus,${NC}" + echo -e " ${DIM}and play it back to verify your audio pipeline works.${NC}\n" + + mkdir -p "$AUDIO_DIR" + + # Step 1: Record + echo -ne " ${YELLOW}● Recording for 3 seconds... speak now!${NC} " + local _tid=$(uid) + local raw_file="$AUDIO_DIR/test_${_tid}.tmp" + audio_record "$raw_file" 3 + echo -e "${GREEN}done${NC}" + + if [ ! -s "$raw_file" ]; then + log_err "Recording failed — check your microphone" + return 1 + fi + + local raw_size + raw_size=$(stat -c%s "$raw_file") + echo -e " ${DIM}Recorded $raw_size bytes of raw audio${NC}" + + # Step 2: Encode with Opus + echo -ne " ${YELLOW}● Encoding with Opus at ${OPUS_BITRATE}kbps...${NC} " + local opus_file="$AUDIO_DIR/test_${_tid}.tmp" + opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \ + --bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \ + --speech --quiet \ + "$raw_file" "$opus_file" 2>/dev/null + echo -e "${GREEN}done${NC}" + + if [ ! -s "$opus_file" ]; then + log_err "Opus encoding failed" + rm -f "$raw_file" + return 1 + fi + + local opus_size + opus_size=$(stat -c%s "$opus_file") + echo -e " ${DIM}Opus size: $opus_size bytes (compression ratio: $((raw_size / opus_size))x)${NC}" + + # Step 3: Encrypt + Decrypt round-trip (if secret is set) + if [ -n "$SHARED_SECRET" ]; then + echo -ne " ${YELLOW}● Encrypting and decrypting...${NC} " + local enc_file="$AUDIO_DIR/test_enc_${_tid}.tmp" + local dec_file="$AUDIO_DIR/test_dec_${_tid}.tmp" + encrypt_file "$opus_file" "$enc_file" + decrypt_file "$enc_file" "$dec_file" + + if cmp -s "$opus_file" "$dec_file"; then + echo -e "${GREEN}encryption round-trip OK${NC}" else - audio_deps+=(arecord aplay) - fi - for dep in "${audio_deps[@]}"; do - if ! check_dep "$dep"; then - log_err "$dep not found — run option 7 to install dependencies first" - missing=1 - fi - done - if [ $missing -eq 1 ]; then - return 1 + echo -e "${RED}encryption round-trip FAILED${NC}" fi + rm -f "$enc_file" + opus_file="$dec_file" + fi - echo -e " ${DIM}This will record 3 seconds of audio, encode it with Opus,${NC}" - echo -e " ${DIM}and play it back to verify your audio pipeline works.${NC}\n" + # Step 4: Decode and play + echo -ne " ${YELLOW}● Playing back...${NC} " + play_chunk "$opus_file" + echo -e "${GREEN}done${NC}" - mkdir -p "$AUDIO_DIR" + rm -f "$raw_file" "$opus_file" "$AUDIO_DIR/test_dec_${_tid}.tmp" 2>/dev/null - # Step 1: Record - echo -ne " ${YELLOW}● Recording for 3 seconds... speak now!${NC} " - local _tid=$(uid) - local raw_file="$AUDIO_DIR/test_${_tid}.tmp" - audio_record "$raw_file" 3 - echo -e "${GREEN}done${NC}" - - if [ ! -s "$raw_file" ]; then - log_err "Recording failed — check your microphone" - return 1 - fi - - local raw_size - raw_size=$(stat -c%s "$raw_file") - echo -e " ${DIM}Recorded $raw_size bytes of raw audio${NC}" - - # Step 2: Encode with Opus - echo -ne " ${YELLOW}● Encoding with Opus at ${OPUS_BITRATE}kbps...${NC} " - local opus_file="$AUDIO_DIR/test_${_tid}.tmp" - opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \ - --bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \ - --speech --quiet \ - "$raw_file" "$opus_file" 2>/dev/null - echo -e "${GREEN}done${NC}" - - if [ ! -s "$opus_file" ]; then - log_err "Opus encoding failed" - rm -f "$raw_file" - return 1 - fi - - local opus_size - opus_size=$(stat -c%s "$opus_file") - echo -e " ${DIM}Opus size: $opus_size bytes (compression ratio: $((raw_size / opus_size))x)${NC}" - - # Step 3: Encrypt + Decrypt round-trip (if secret is set) - if [ -n "$SHARED_SECRET" ]; then - echo -ne " ${YELLOW}● Encrypting and decrypting...${NC} " - local enc_file="$AUDIO_DIR/test_enc_${_tid}.tmp" - local dec_file="$AUDIO_DIR/test_dec_${_tid}.tmp" - encrypt_file "$opus_file" "$enc_file" - decrypt_file "$enc_file" "$dec_file" - - if cmp -s "$opus_file" "$dec_file"; then - echo -e "${GREEN}encryption round-trip OK${NC}" - else - echo -e "${RED}encryption round-trip FAILED${NC}" - fi - rm -f "$enc_file" - opus_file="$dec_file" - fi - - # Step 4: Decode and play - echo -ne " ${YELLOW}● Playing back...${NC} " - play_chunk "$opus_file" - echo -e "${GREEN}done${NC}" - - rm -f "$raw_file" "$opus_file" "$AUDIO_DIR/test_dec_${_tid}.tmp" 2>/dev/null - - echo -e "\n ${GREEN}${BOLD}Audio test complete!${NC}" - echo -e " ${DIM}If you heard your voice, the pipeline is working.${NC}\n" + echo -e "\n ${GREEN}${BOLD}Audio test complete!${NC}" + echo -e " ${DIM}If you heard your voice, the pipeline is working.${NC}\n" } #============================================================================= @@ -1987,53 +1988,53 @@ test_audio() { #============================================================================= show_status() { - echo -e "\n${BOLD}${CYAN}═══ Status ═══${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Status ═══${NC}\n" - # Tor status - if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then - echo -e " ${GREEN}●${NC} Tor running (PID $TOR_PID)" - local onion - onion=$(get_onion) - if [ -n "$onion" ]; then - echo -e " ${BOLD}${WHITE} Address: ${onion}${NC}" - fi - else - echo -e " ${RED}●${NC} Tor not running" + # Tor status + if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then + echo -e " ${GREEN}●${NC} Tor running (PID $TOR_PID)" + local onion + onion=$(get_onion) + if [ -n "$onion" ]; then + echo -e " ${BOLD}${WHITE} Address: ${onion}${NC}" fi + else + echo -e " ${RED}●${NC} Tor not running" + fi - # Secret - if [ -n "$SHARED_SECRET" ]; then - echo -e " ${GREEN}●${NC} Shared secret set" + # Secret + if [ -n "$SHARED_SECRET" ]; then + echo -e " ${GREEN}●${NC} Shared secret set" + else + echo -e " ${RED}●${NC} No shared secret (set one before calling)" + fi + + # Audio + if check_dep arecord && check_dep opusenc; then + echo -e " ${GREEN}●${NC} Audio pipeline ready" + else + echo -e " ${RED}●${NC} Audio dependencies missing" + fi + + # Snowflake + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + if check_dep snowflake-client; then + echo -e " ${GREEN}●${NC} Snowflake bridge enabled" else - echo -e " ${RED}●${NC} No shared secret (set one before calling)" + echo -e " ${YELLOW}●${NC} Snowflake enabled (client not installed)" fi + else + echo -e " ${DIM}●${NC} Snowflake bridge disabled" + fi - # Audio - if check_dep arecord && check_dep opusenc; then - echo -e " ${GREEN}●${NC} Audio pipeline ready" - else - echo -e " ${RED}●${NC} Audio dependencies missing" - fi - - # Snowflake - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - if check_dep snowflake-client; then - echo -e " ${GREEN}●${NC} Snowflake bridge enabled" - else - echo -e " ${YELLOW}●${NC} Snowflake enabled (client not installed)" - fi - else - echo -e " ${DIM}●${NC} Snowflake bridge disabled" - fi - - # Config - echo -e "\n ${DIM}Listen port: $LISTEN_PORT${NC}" - echo -e " ${DIM}SOCKS port: $TOR_SOCKS_PORT${NC}" - echo -e " ${DIM}Cipher: $CIPHER${NC}" - echo -e " ${DIM}Opus bitrate: ${OPUS_BITRATE}kbps${NC}" - echo -e " ${DIM}Opus frame: ${OPUS_FRAMESIZE}ms${NC}" - echo -e " ${DIM}PTT key: [SPACEBAR]${NC}" - echo "" + # Config + echo -e "\n ${DIM}Listen port: $LISTEN_PORT${NC}" + echo -e " ${DIM}SOCKS port: $TOR_SOCKS_PORT${NC}" + echo -e " ${DIM}Cipher: $CIPHER${NC}" + echo -e " ${DIM}Opus bitrate: ${OPUS_BITRATE}kbps${NC}" + echo -e " ${DIM}Opus frame: ${OPUS_FRAMESIZE}ms${NC}" + echo -e " ${DIM}PTT key: [SPACEBAR]${NC}" + echo "" } #============================================================================= @@ -2041,778 +2042,778 @@ show_status() { #============================================================================= settings_menu() { - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ Settings ═══${NC}\n" - echo -e " ${DIM}Current Opus bitrate: ${NC}${WHITE}${OPUS_BITRATE} kbps${NC}" - echo -e " ${DIM}Current Opus frame: ${NC}${WHITE}${OPUS_FRAMESIZE} ms${NC}" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ Settings ═══${NC}\n" + echo -e " ${DIM}Current Opus bitrate: ${NC}${WHITE}${OPUS_BITRATE} kbps${NC}" + echo -e " ${DIM}Current Opus frame: ${NC}${WHITE}${OPUS_FRAMESIZE} ms${NC}" - local al_label="${RED}disabled${NC}" - if [ "$AUTO_LISTEN" -eq 1 ]; then - al_label="${GREEN}enabled${NC}" - fi - echo -e " ${DIM}Auto-listen: ${NC}${al_label}" + local al_label="${RED}disabled${NC}" + if [ "$AUTO_LISTEN" -eq 1 ]; then + al_label="${GREEN}enabled${NC}" + fi + echo -e " ${DIM}Auto-listen: ${NC}${al_label}" - local ptt_display="SPACE" - [ "$PTT_KEY" != " " ] && ptt_display="$PTT_KEY" - echo -e " ${DIM}PTT key: ${NC}${WHITE}${ptt_display}${NC}" + local ptt_display="SPACE" + [ "$PTT_KEY" != " " ] && ptt_display="$PTT_KEY" + echo -e " ${DIM}PTT key: ${NC}${WHITE}${ptt_display}${NC}" - local vfx_display="${VOICE_EFFECT}" - [ "$vfx_display" = "none" ] && vfx_display="off" - echo -e " ${DIM}Voice effect: ${NC}${WHITE}${vfx_display}${NC}" + local vfx_display="${VOICE_EFFECT}" + [ "$vfx_display" = "none" ] && vfx_display="off" + echo -e " ${DIM}Voice effect: ${NC}${WHITE}${vfx_display}${NC}" - if [ $IS_TERMUX -eq 1 ]; then - local vp_label="${RED}disabled${NC}" - if [ "$VOL_PTT" -eq 1 ]; then - vp_label="${GREEN}enabled${NC}" + if [ $IS_TERMUX -eq 1 ]; then + local vp_label="${RED}disabled${NC}" + if [ "$VOL_PTT" -eq 1 ]; then + vp_label="${GREEN}enabled${NC}" + fi + echo -e " ${DIM}Volume PTT: ${NC}${vp_label} ${DIM}(experimental)${NC}" + fi + + local circ_label="${RED}disabled${NC}" + if [ "$SHOW_CIRCUIT" -eq 1 ]; then + circ_label="${GREEN}enabled${NC}" + fi + echo -e " ${DIM}Circuit display: ${NC}${circ_label}" + + local hmac_label="${RED}disabled${NC}" + if [ "$HMAC_AUTH" -eq 1 ]; then + hmac_label="${GREEN}enabled${NC}" + fi + echo -e " ${DIM}HMAC auth: ${NC}${hmac_label}" + echo "" + + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Change Opus encoding quality" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Auto-listen (listen for calls automatically once Tor starts)" + echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Change PTT (push-to-talk) key" + echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Voice changer" + if [ $IS_TERMUX -eq 1 ]; then + echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Volume PTT ${DIM}(double-tap Vol Down to talk, experimental)${NC}" + fi + echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Tor settings" + echo -e " ${BOLD}${WHITE}7${NC} ${CYAN}│${NC} Security" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back to main menu${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r schoice + + case "$schoice" in + 1) settings_opus ;; + 2) + if [ "$AUTO_LISTEN" -eq 1 ]; then + AUTO_LISTEN=0 + stop_auto_listener + log_ok "Auto-listen disabled" + else + AUTO_LISTEN=1 + log_ok "Auto-listen enabled" + start_auto_listener + fi + save_config + sleep 1 + ;; + 3) + local _pd="SPACE" + [ "$PTT_KEY" != " " ] && _pd="$PTT_KEY" + echo -e "\n ${DIM}Current PTT key: ${NC}${WHITE}${_pd}${NC}" + echo -ne " ${BOLD}Press the key you want to use for PTT: ${NC}" + # Read a single character in raw mode + local _old_stty + _old_stty=$(stty -g) + stty raw -echo + local _newkey + _newkey=$(dd bs=1 count=1 2>/dev/null) || true + stty "$_old_stty" + if [ -n "$_newkey" ]; then + PTT_KEY="$_newkey" + save_config + local _nd="SPACE" + [ "$PTT_KEY" != " " ] && _nd="$PTT_KEY" + log_ok "PTT key set to: ${_nd}" + fi + sleep 1 + ;; + 4) settings_voice ;; + 5) + if [ $IS_TERMUX -eq 1 ]; then + if [ "$VOL_PTT" -eq 1 ]; then + VOL_PTT=0 + log_ok "Volume PTT disabled" + else + if ! check_dep jq; then + echo "" + echo -ne " ${BOLD}jq is required for Volume PTT. Install now? [Y/n]: ${NC}" + read -r _jq_confirm + if [ "$_jq_confirm" != "n" ] && [ "$_jq_confirm" != "N" ]; then + pkg install -y jq 2>/dev/null || true + if ! check_dep jq; then + log_err "jq installation failed — Volume PTT not enabled" + sleep 2 + continue + fi + else + echo -e "\n ${YELLOW}Volume PTT not enabled (jq not installed)${NC}" + sleep 2 + continue fi - echo -e " ${DIM}Volume PTT: ${NC}${vp_label} ${DIM}(experimental)${NC}" + fi + VOL_PTT=1 + log_ok "Volume PTT enabled (double-tap Vol Down to toggle recording)" + echo -e " ${DIM}Note: Each press will lower your device volume.${NC}" + echo -e " ${DIM}You may want to start with volume at max.${NC}" fi - - local circ_label="${RED}disabled${NC}" - if [ "$SHOW_CIRCUIT" -eq 1 ]; then - circ_label="${GREEN}enabled${NC}" - fi - echo -e " ${DIM}Circuit display: ${NC}${circ_label}" - - local hmac_label="${RED}disabled${NC}" - if [ "$HMAC_AUTH" -eq 1 ]; then - hmac_label="${GREEN}enabled${NC}" - fi - echo -e " ${DIM}HMAC auth: ${NC}${hmac_label}" - echo "" - - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Change Opus encoding quality" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Auto-listen (listen for calls automatically once Tor starts)" - echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Change PTT (push-to-talk) key" - echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Voice changer" - if [ $IS_TERMUX -eq 1 ]; then - echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Volume PTT ${DIM}(double-tap Vol Down to talk, experimental)${NC}" - fi - echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Tor settings" - echo -e " ${BOLD}${WHITE}7${NC} ${CYAN}│${NC} Security" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back to main menu${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r schoice - - case "$schoice" in - 1) settings_opus ;; - 2) - if [ "$AUTO_LISTEN" -eq 1 ]; then - AUTO_LISTEN=0 - stop_auto_listener - log_ok "Auto-listen disabled" - else - AUTO_LISTEN=1 - log_ok "Auto-listen enabled" - start_auto_listener - fi - save_config - sleep 1 - ;; - 3) - local _pd="SPACE" - [ "$PTT_KEY" != " " ] && _pd="$PTT_KEY" - echo -e "\n ${DIM}Current PTT key: ${NC}${WHITE}${_pd}${NC}" - echo -ne " ${BOLD}Press the key you want to use for PTT: ${NC}" - # Read a single character in raw mode - local _old_stty - _old_stty=$(stty -g) - stty raw -echo - local _newkey - _newkey=$(dd bs=1 count=1 2>/dev/null) || true - stty "$_old_stty" - if [ -n "$_newkey" ]; then - PTT_KEY="$_newkey" - save_config - local _nd="SPACE" - [ "$PTT_KEY" != " " ] && _nd="$PTT_KEY" - log_ok "PTT key set to: ${_nd}" - fi - sleep 1 - ;; - 4) settings_voice ;; - 5) - if [ $IS_TERMUX -eq 1 ]; then - if [ "$VOL_PTT" -eq 1 ]; then - VOL_PTT=0 - log_ok "Volume PTT disabled" - else - if ! check_dep jq; then - echo "" - echo -ne " ${BOLD}jq is required for Volume PTT. Install now? [Y/n]: ${NC}" - read -r _jq_confirm - if [ "$_jq_confirm" != "n" ] && [ "$_jq_confirm" != "N" ]; then - pkg install -y jq 2>/dev/null || true - if ! check_dep jq; then - log_err "jq installation failed — Volume PTT not enabled" - sleep 2 - continue - fi - else - echo -e "\n ${YELLOW}Volume PTT not enabled (jq not installed)${NC}" - sleep 2 - continue - fi - fi - VOL_PTT=1 - log_ok "Volume PTT enabled (double-tap Vol Down to toggle recording)" - echo -e " ${DIM}Note: Each press will lower your device volume.${NC}" - echo -e " ${DIM}You may want to start with volume at max.${NC}" - fi - save_config - sleep 2 - else - echo -e "\n ${RED}Volume PTT is only available in Termux${NC}" - sleep 1 - fi - ;; - 6) settings_tor ;; - 7) settings_security ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + save_config + sleep 2 + else + echo -e "\n ${RED}Volume PTT is only available in Termux${NC}" + sleep 1 + fi + ;; + 6) settings_tor ;; + 7) settings_security ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } settings_cipher() { - echo -e "\n${BOLD}${CYAN}═══ Select Encryption Cipher ═══${NC}\n" - echo -e " ${DIM}Current: ${NC}${GREEN}${CIPHER}${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Select Encryption Cipher ═══${NC}\n" + echo -e " ${DIM}Current: ${NC}${GREEN}${CIPHER}${NC}\n" - # Curated cipher list ranked from strongest to adequate - # Only includes ciphers verified to work with openssl enc -pbkdf2 - # Excludes: ECB modes (pattern leakage), DES/RC2/RC4/Blowfish (weak), aliases - local ciphers=( - # ── 256-bit (Strongest) ── - "aes-256-ctr" - "aes-256-cbc" - "aes-256-cfb" - "aes-256-ofb" - "chacha20" - "camellia-256-ctr" - "camellia-256-cbc" - "aria-256-ctr" - "aria-256-cbc" - # ── 192-bit (Strong) ── - "aes-192-ctr" - "aes-192-cbc" - "camellia-192-ctr" - "camellia-192-cbc" - "aria-192-ctr" - "aria-192-cbc" - # ── 128-bit (Adequate) ── - "aes-128-ctr" - "aes-128-cbc" - "camellia-128-ctr" - "camellia-128-cbc" - "aria-128-ctr" - "aria-128-cbc" - ) + # Curated cipher list ranked from strongest to adequate + # Only includes ciphers verified to work with openssl enc -pbkdf2 + # Excludes: ECB modes (pattern leakage), DES/RC2/RC4/Blowfish (weak), aliases + local ciphers=( + # ── 256-bit (Strongest) ── + "aes-256-ctr" + "aes-256-cbc" + "aes-256-cfb" + "aes-256-ofb" + "chacha20" + "camellia-256-ctr" + "camellia-256-cbc" + "aria-256-ctr" + "aria-256-cbc" + # ── 192-bit (Strong) ── + "aes-192-ctr" + "aes-192-cbc" + "camellia-192-ctr" + "camellia-192-cbc" + "aria-192-ctr" + "aria-192-cbc" + # ── 128-bit (Adequate) ── + "aes-128-ctr" + "aes-128-cbc" + "camellia-128-ctr" + "camellia-128-cbc" + "aria-128-ctr" + "aria-128-cbc" + ) - local total=${#ciphers[@]} + local total=${#ciphers[@]} - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ Available Ciphers ═══${NC}" - echo -e " ${DIM}Current: ${NC}${GREEN}${CIPHER}${NC}" - echo -e " ${DIM}${total} ciphers, ranked strongest → adequate${NC}\n" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ Available Ciphers ═══${NC}" + echo -e " ${DIM}Current: ${NC}${GREEN}${CIPHER}${NC}" + echo -e " ${DIM}${total} ciphers, ranked strongest → adequate${NC}\n" - local tier="" - for ((i = 0; i < total; i++)); do - local num=$((i + 1)) - local c="${ciphers[$i]}" + local tier="" + for ((i = 0; i < total; i++)); do + local num=$((i + 1)) + local c="${ciphers[$i]}" - # Print tier headers - if [ $i -eq 0 ]; then - echo -e " ${GREEN}${BOLD}── 256-bit (Strongest) ──${NC}" - elif [ $i -eq 9 ]; then - echo -e " ${YELLOW}${BOLD}── 192-bit (Strong) ──${NC}" - elif [ $i -eq 15 ]; then - echo -e " ${WHITE}${BOLD}── 128-bit (Adequate) ──${NC}" - fi + # Print tier headers + if [ $i -eq 0 ]; then + echo -e " ${GREEN}${BOLD}── 256-bit (Strongest) ──${NC}" + elif [ $i -eq 9 ]; then + echo -e " ${YELLOW}${BOLD}── 192-bit (Strong) ──${NC}" + elif [ $i -eq 15 ]; then + echo -e " ${WHITE}${BOLD}── 128-bit (Adequate) ──${NC}" + fi - if [ "$c" = "$CIPHER" ]; then - printf " ${GREEN}${BOLD}%4d${NC} ${CYAN}│${NC} ${GREEN}%-30s ◄ current${NC}\n" "$num" "$c" - else - printf " ${WHITE}${BOLD}%4d${NC} ${CYAN}│${NC} %-30s\n" "$num" "$c" - fi - done - - echo "" - echo -e " ${DIM}[0] cancel${NC}" - echo -ne " ${BOLD}Enter cipher number: ${NC}" - read -r cinput - - case "$cinput" in - 0|q|Q) - return - ;; - '') - ;; - *) - if [[ "$cinput" =~ ^[0-9]+$ ]] && [ "$cinput" -ge 1 ] && [ "$cinput" -le "$total" ]; then - local selected="${ciphers[$((cinput - 1))]}" - # Validate that openssl can actually use this cipher - if echo "test" | openssl enc -"${selected}" -pbkdf2 -pass pass:test 2>/dev/null | openssl enc -d -"${selected}" -pbkdf2 -pass pass:test &>/dev/null; then - CIPHER="$selected" - save_config - # Update runtime file for live mid-call sync - [ -f "$CIPHER_RUNTIME_FILE" ] && echo "$CIPHER" > "$CIPHER_RUNTIME_FILE" - # Notify remote side if in a call - if [ "$CALL_ACTIVE" -eq 1 ]; then - proto_send "CIPHER:${CIPHER}" - fi - echo -e "\n ${GREEN}${BOLD}✓${NC} Cipher set to ${WHITE}${BOLD}${CIPHER}${NC}" - else - echo -e "\n ${RED}${BOLD}✗${NC} Cipher '${selected}' failed validation — not compatible with stream encryption" - fi - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r - return - else - echo -e "\n ${RED}Invalid number${NC}" - sleep 1 - fi - ;; - esac + if [ "$c" = "$CIPHER" ]; then + printf " ${GREEN}${BOLD}%4d${NC} ${CYAN}│${NC} ${GREEN}%-30s ◄ current${NC}\n" "$num" "$c" + else + printf " ${WHITE}${BOLD}%4d${NC} ${CYAN}│${NC} %-30s\n" "$num" "$c" + fi done + + echo "" + echo -e " ${DIM}[0] cancel${NC}" + echo -ne " ${BOLD}Enter cipher number: ${NC}" + read -r cinput + + case "$cinput" in + 0 | q | Q) + return + ;; + '') ;; + *) + if [[ "$cinput" =~ ^[0-9]+$ ]] && [ "$cinput" -ge 1 ] && [ "$cinput" -le "$total" ]; then + local selected="${ciphers[$((cinput - 1))]}" + # Validate that openssl can actually use this cipher + if echo "test" | openssl enc -"${selected}" -pbkdf2 -pass pass:test 2>/dev/null | openssl enc -d -"${selected}" -pbkdf2 -pass pass:test &>/dev/null; then + CIPHER="$selected" + save_config + # Update runtime file for live mid-call sync + [ -f "$CIPHER_RUNTIME_FILE" ] && echo "$CIPHER" >"$CIPHER_RUNTIME_FILE" + # Notify remote side if in a call + if [ "$CALL_ACTIVE" -eq 1 ]; then + proto_send "CIPHER:${CIPHER}" + fi + echo -e "\n ${GREEN}${BOLD}✓${NC} Cipher set to ${WHITE}${BOLD}${CIPHER}${NC}" + else + echo -e "\n ${RED}${BOLD}✗${NC} Cipher '${selected}' failed validation — not compatible with stream encryption" + fi + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r + return + else + echo -e "\n ${RED}Invalid number${NC}" + sleep 1 + fi + ;; + esac + done } settings_opus() { - echo -e "\n${BOLD}${CYAN}═══ Opus Encoding Quality ═══${NC}\n" - echo -e " ${DIM}Current bitrate: ${NC}${GREEN}${OPUS_BITRATE} kbps${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Opus Encoding Quality ═══${NC}\n" + echo -e " ${DIM}Current bitrate: ${NC}${GREEN}${OPUS_BITRATE} kbps${NC}\n" - local -a presets=(6 8 12 16 24 32 48 64) - local -a labels=( - "6 kbps — Minimum (very low bandwidth)" - "8 kbps — Low (narrowband voice)" - "12 kbps — Medium-Low (clear voice)" - "16 kbps — Medium (recommended for Tor)" - "24 kbps — Medium-High (good quality)" - "32 kbps — High (wideband voice)" - "48 kbps — Very High (near-studio)" - "64 kbps — Maximum (best quality)" - ) + local -a presets=(6 8 12 16 24 32 48 64) + local -a labels=( + "6 kbps — Minimum (very low bandwidth)" + "8 kbps — Low (narrowband voice)" + "12 kbps — Medium-Low (clear voice)" + "16 kbps — Medium (recommended for Tor)" + "24 kbps — Medium-High (good quality)" + "32 kbps — High (wideband voice)" + "48 kbps — Very High (near-studio)" + "64 kbps — Maximum (best quality)" + ) - for ((i = 0; i < ${#presets[@]}; i++)); do - local num=$((i + 1)) - if [ "${presets[$i]}" = "$OPUS_BITRATE" ]; then - echo -e " ${GREEN}${BOLD}${num}${NC} ${CYAN}│${NC} ${GREEN}${labels[$i]} ◄ current${NC}" - else - echo -e " ${BOLD}${WHITE}${num}${NC} ${CYAN}│${NC} ${labels[$i]}" - fi - done + for ((i = 0; i < ${#presets[@]}; i++)); do + local num=$((i + 1)) + if [ "${presets[$i]}" = "$OPUS_BITRATE" ]; then + echo -e " ${GREEN}${BOLD}${num}${NC} ${CYAN}│${NC} ${GREEN}${labels[$i]} ◄ current${NC}" + else + echo -e " ${BOLD}${WHITE}${num}${NC} ${CYAN}│${NC} ${labels[$i]}" + fi + done - echo -e " ${BOLD}${WHITE}9${NC} ${CYAN}│${NC} Custom bitrate" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Cancel${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r oinput + echo -e " ${BOLD}${WHITE}9${NC} ${CYAN}│${NC} Custom bitrate" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Cancel${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r oinput - case "$oinput" in - [1-8]) - OPUS_BITRATE=${presets[$((oinput - 1))]} - save_config - echo -e "\n ${GREEN}${BOLD}✓${NC} Opus bitrate set to ${WHITE}${BOLD}${OPUS_BITRATE} kbps${NC}" - ;; - 9) - echo -ne "\n ${BOLD}Enter bitrate (6-510 kbps): ${NC}" - read -r custom_br - if [[ "$custom_br" =~ ^[0-9]+$ ]] && [ "$custom_br" -ge 6 ] && [ "$custom_br" -le 510 ]; then - OPUS_BITRATE=$custom_br - save_config - echo -e "\n ${GREEN}${BOLD}✓${NC} Opus bitrate set to ${WHITE}${BOLD}${OPUS_BITRATE} kbps${NC}" - else - echo -e "\n ${RED}Invalid bitrate. Must be 6–510.${NC}" - fi - ;; - 0|q|Q) - return - ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - ;; - esac - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r + case "$oinput" in + [1-8]) + OPUS_BITRATE=${presets[$((oinput - 1))]} + save_config + echo -e "\n ${GREEN}${BOLD}✓${NC} Opus bitrate set to ${WHITE}${BOLD}${OPUS_BITRATE} kbps${NC}" + ;; + 9) + echo -ne "\n ${BOLD}Enter bitrate (6-510 kbps): ${NC}" + read -r custom_br + if [[ "$custom_br" =~ ^[0-9]+$ ]] && [ "$custom_br" -ge 6 ] && [ "$custom_br" -le 510 ]; then + OPUS_BITRATE=$custom_br + save_config + echo -e "\n ${GREEN}${BOLD}✓${NC} Opus bitrate set to ${WHITE}${BOLD}${OPUS_BITRATE} kbps${NC}" + else + echo -e "\n ${RED}Invalid bitrate. Must be 6–510.${NC}" + fi + ;; + 0 | q | Q) + return + ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + ;; + esac + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r } settings_snowflake() { - clear - echo -e "\n${BOLD}${CYAN}═══ Snowflake Bridge ═══${NC}\n" - echo -e " ${DIM}Snowflake uses WebRTC proxies to help bypass Tor censorship.${NC}" - echo -e " ${DIM}Enable this if Tor is blocked in your region.${NC}\n" + clear + echo -e "\n${BOLD}${CYAN}═══ Snowflake Bridge ═══${NC}\n" + echo -e " ${DIM}Snowflake uses WebRTC proxies to help bypass Tor censorship.${NC}" + echo -e " ${DIM}Enable this if Tor is blocked in your region.${NC}\n" + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + echo -e " Status: ${GREEN}${BOLD}ENABLED${NC}" + else + echo -e " Status: ${RED}${BOLD}DISABLED${NC}" + fi + + if check_dep snowflake-client; then + echo -e " Binary: ${GREEN}●${NC} snowflake-client installed" + else + echo -e " Binary: ${RED}●${NC} snowflake-client not installed" + fi + + echo -e " ${DIM}Tor manages snowflake-client as a pluggable transport.${NC}" + + echo "" + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Disable Snowflake" + else + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Enable Snowflake" + fi + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r sf_choice + + case "$sf_choice" in + 1) if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - echo -e " Status: ${GREEN}${BOLD}ENABLED${NC}" + SNOWFLAKE_ENABLED=0 + save_config + + echo -e "\n ${YELLOW}${BOLD}✓${NC} Snowflake disabled" + echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" else - echo -e " Status: ${RED}${BOLD}DISABLED${NC}" + # Install if not present + if ! check_dep snowflake-client; then + echo "" + echo -ne " ${BOLD}snowflake-client not installed. Install now? [Y/n]: ${NC}" + read -r install_confirm + if [ "$install_confirm" != "n" ] && [ "$install_confirm" != "N" ]; then + install_snowflake || { + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r + return + } + else + echo -e "\n ${YELLOW}Snowflake not enabled (client not installed)${NC}" + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r + return + fi + fi + SNOWFLAKE_ENABLED=1 + save_config + echo -e "\n ${GREEN}${BOLD}✓${NC} Snowflake enabled" + echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" fi - - if check_dep snowflake-client; then - echo -e " Binary: ${GREEN}●${NC} snowflake-client installed" - else - echo -e " Binary: ${RED}●${NC} snowflake-client not installed" - fi - - echo -e " ${DIM}Tor manages snowflake-client as a pluggable transport.${NC}" - - echo "" - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Disable Snowflake" - else - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Enable Snowflake" - fi - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r sf_choice - - case "$sf_choice" in - 1) - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - SNOWFLAKE_ENABLED=0 - save_config - - echo -e "\n ${YELLOW}${BOLD}✓${NC} Snowflake disabled" - echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" - else - # Install if not present - if ! check_dep snowflake-client; then - echo "" - echo -ne " ${BOLD}snowflake-client not installed. Install now? [Y/n]: ${NC}" - read -r install_confirm - if [ "$install_confirm" != "n" ] && [ "$install_confirm" != "N" ]; then - install_snowflake || { - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r - return - } - else - echo -e "\n ${YELLOW}Snowflake not enabled (client not installed)${NC}" - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r - return - fi - fi - SNOWFLAKE_ENABLED=1 - save_config - echo -e "\n ${GREEN}${BOLD}✓${NC} Snowflake enabled" - echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" - fi - ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - ;; - esac - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r + ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + ;; + esac + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r } settings_voice() { - echo -e "\n${BOLD}${CYAN}═══ Voice Changer ═══${NC}\n" - echo -e " ${DIM}Current effect: ${NC}${GREEN}${VOICE_EFFECT}${NC}\n" + echo -e "\n${BOLD}${CYAN}═══ Voice Changer ═══${NC}\n" + echo -e " ${DIM}Current effect: ${NC}${GREEN}${VOICE_EFFECT}${NC}\n" - local effects=("none" "deep" "high" "robot" "echo" "whisper" "custom") - local descs=( - "No effect (natural voice)" - "Deep voice (pitch shifted down)" - "High voice (pitch shifted up)" - "Robot (overdrive + flanger)" - "Echo (delayed reverb)" - "Whisper (highpass + tremolo)" - "Custom (configure all parameters)" - ) + local effects=("none" "deep" "high" "robot" "echo" "whisper" "custom") + local descs=( + "No effect (natural voice)" + "Deep voice (pitch shifted down)" + "High voice (pitch shifted up)" + "Robot (overdrive + flanger)" + "Echo (delayed reverb)" + "Whisper (highpass + tremolo)" + "Custom (configure all parameters)" + ) - local i - for i in "${!effects[@]}"; do - local num=$(( i + 1 )) - local marker=" " - if [ "${effects[$i]}" = "$VOICE_EFFECT" ]; then - marker="${GREEN}> ${NC}" - fi - echo -e " ${marker}${BOLD}${WHITE}${num}${NC} ${CYAN}│${NC} ${descs[$i]}" - done + local i + for i in "${!effects[@]}"; do + local num=$((i + 1)) + local marker=" " + if [ "${effects[$i]}" = "$VOICE_EFFECT" ]; then + marker="${GREEN}> ${NC}" + fi + echo -e " ${marker}${BOLD}${WHITE}${num}${NC} ${CYAN}│${NC} ${descs[$i]}" + done - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r vchoice + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r vchoice - case "$vchoice" in - 1) VOICE_EFFECT="none" ;; - 2) VOICE_EFFECT="deep" ;; - 3) VOICE_EFFECT="high" ;; - 4) VOICE_EFFECT="robot" ;; - 5) VOICE_EFFECT="echo" ;; - 6) VOICE_EFFECT="whisper" ;; - 7) VOICE_EFFECT="custom" - settings_voice_custom - ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - return - ;; - esac - save_config - log_ok "Voice effect set to: ${VOICE_EFFECT}" + case "$vchoice" in + 1) VOICE_EFFECT="none" ;; + 2) VOICE_EFFECT="deep" ;; + 3) VOICE_EFFECT="high" ;; + 4) VOICE_EFFECT="robot" ;; + 5) VOICE_EFFECT="echo" ;; + 6) VOICE_EFFECT="whisper" ;; + 7) + VOICE_EFFECT="custom" + settings_voice_custom + ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" sleep 1 + return + ;; + esac + save_config + log_ok "Voice effect set to: ${VOICE_EFFECT}" + sleep 1 } settings_voice_custom() { - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ Custom Voice Effect ═══${NC}\n" - echo -e " ${DIM}Configure each parameter. Effects are combined into one chain.${NC}" - echo -e " ${DIM}Set a value to 0 to disable that effect.${NC}\n" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ Custom Voice Effect ═══${NC}\n" + echo -e " ${DIM}Configure each parameter. Effects are combined into one chain.${NC}" + echo -e " ${DIM}Set a value to 0 to disable that effect.${NC}\n" - local _p_status="${RED}off${NC}" - [ "$VOICE_PITCH" -ne 0 ] 2>/dev/null && _p_status="${GREEN}${VOICE_PITCH} cents${NC}" - local _od_status="${RED}off${NC}" - [ "$VOICE_OVERDRIVE" -gt 0 ] 2>/dev/null && _od_status="${GREEN}${VOICE_OVERDRIVE}${NC}" - local _fl_status="${RED}off${NC}" - [ "$VOICE_FLANGER" -eq 1 ] 2>/dev/null && _fl_status="${GREEN}on${NC}" - local _ed_status="${RED}off${NC}" - [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null && _ed_status="${GREEN}${VOICE_ECHO_DELAY}ms decay 0.${VOICE_ECHO_DECAY}${NC}" - local _hp_status="${RED}off${NC}" - [ "$VOICE_HIGHPASS" -gt 0 ] 2>/dev/null && _hp_status="${GREEN}${VOICE_HIGHPASS} Hz${NC}" - local _tr_status="${RED}off${NC}" - [ "$VOICE_TREMOLO" -gt 0 ] 2>/dev/null && _tr_status="${GREEN}${VOICE_TREMOLO} Hz${NC}" + local _p_status="${RED}off${NC}" + [ "$VOICE_PITCH" -ne 0 ] 2>/dev/null && _p_status="${GREEN}${VOICE_PITCH} cents${NC}" + local _od_status="${RED}off${NC}" + [ "$VOICE_OVERDRIVE" -gt 0 ] 2>/dev/null && _od_status="${GREEN}${VOICE_OVERDRIVE}${NC}" + local _fl_status="${RED}off${NC}" + [ "$VOICE_FLANGER" -eq 1 ] 2>/dev/null && _fl_status="${GREEN}on${NC}" + local _ed_status="${RED}off${NC}" + [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null && _ed_status="${GREEN}${VOICE_ECHO_DELAY}ms decay 0.${VOICE_ECHO_DECAY}${NC}" + local _hp_status="${RED}off${NC}" + [ "$VOICE_HIGHPASS" -gt 0 ] 2>/dev/null && _hp_status="${GREEN}${VOICE_HIGHPASS} Hz${NC}" + local _tr_status="${RED}off${NC}" + [ "$VOICE_TREMOLO" -gt 0 ] 2>/dev/null && _tr_status="${GREEN}${VOICE_TREMOLO} Hz${NC}" - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Pitch shift ${_p_status} ${DIM}(-600 to +600 cents)${NC}" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Overdrive ${_od_status} ${DIM}(0=off, 5-20)${NC}" - echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Flanger ${_fl_status} ${DIM}(0=off, 1=on)${NC}" - echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Echo ${_ed_status} ${DIM}(delay 0-200ms, decay 1-9)${NC}" - echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Highpass filter ${_hp_status} ${DIM}(0=off, 300-2000 Hz)${NC}" - echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Tremolo ${_tr_status} ${DIM}(0=off, 5-40 Hz)${NC}" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Done${NC}" - echo "" - echo -ne " ${BOLD}Select parameter: ${NC}" - read -r pchoice + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Pitch shift ${_p_status} ${DIM}(-600 to +600 cents)${NC}" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Overdrive ${_od_status} ${DIM}(0=off, 5-20)${NC}" + echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Flanger ${_fl_status} ${DIM}(0=off, 1=on)${NC}" + echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Echo ${_ed_status} ${DIM}(delay 0-200ms, decay 1-9)${NC}" + echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Highpass filter ${_hp_status} ${DIM}(0=off, 300-2000 Hz)${NC}" + echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Tremolo ${_tr_status} ${DIM}(0=off, 5-40 Hz)${NC}" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Done${NC}" + echo "" + echo -ne " ${BOLD}Select parameter: ${NC}" + read -r pchoice - case "$pchoice" in - 1) - echo -ne " ${BOLD}Pitch shift (cents, -600 to +600, 0=off): ${NC}" - read -r val - [[ "$val" =~ ^-?[0-9]+$ ]] && VOICE_PITCH=$val - ;; - 2) - echo -ne " ${BOLD}Overdrive gain (0=off, 5-20): ${NC}" - read -r val - [[ "$val" =~ ^[0-9]+$ ]] && VOICE_OVERDRIVE=$val - ;; - 3) - echo -ne " ${BOLD}Flanger (0=off, 1=on): ${NC}" - read -r val - [[ "$val" =~ ^[01]$ ]] && VOICE_FLANGER=$val - ;; - 4) - echo -ne " ${BOLD}Echo delay (ms, 0=off, 20-200): ${NC}" - read -r val - [[ "$val" =~ ^[0-9]+$ ]] && VOICE_ECHO_DELAY=$val - if [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null; then - echo -ne " ${BOLD}Echo decay (1-9, maps to 0.1-0.9): ${NC}" - read -r val - [[ "$val" =~ ^[1-9]$ ]] && VOICE_ECHO_DECAY=$val - fi - ;; - 5) - echo -ne " ${BOLD}Highpass frequency (Hz, 0=off, 300-2000): ${NC}" - read -r val - [[ "$val" =~ ^[0-9]+$ ]] && VOICE_HIGHPASS=$val - ;; - 6) - echo -ne " ${BOLD}Tremolo speed (Hz, 0=off, 5-40): ${NC}" - read -r val - [[ "$val" =~ ^[0-9]+$ ]] && VOICE_TREMOLO=$val - ;; - 0|q|Q) - save_config - return - ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + case "$pchoice" in + 1) + echo -ne " ${BOLD}Pitch shift (cents, -600 to +600, 0=off): ${NC}" + read -r val + [[ "$val" =~ ^-?[0-9]+$ ]] && VOICE_PITCH=$val + ;; + 2) + echo -ne " ${BOLD}Overdrive gain (0=off, 5-20): ${NC}" + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && VOICE_OVERDRIVE=$val + ;; + 3) + echo -ne " ${BOLD}Flanger (0=off, 1=on): ${NC}" + read -r val + [[ "$val" =~ ^[01]$ ]] && VOICE_FLANGER=$val + ;; + 4) + echo -ne " ${BOLD}Echo delay (ms, 0=off, 20-200): ${NC}" + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && VOICE_ECHO_DELAY=$val + if [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null; then + echo -ne " ${BOLD}Echo decay (1-9, maps to 0.1-0.9): ${NC}" + read -r val + [[ "$val" =~ ^[1-9]$ ]] && VOICE_ECHO_DECAY=$val + fi + ;; + 5) + echo -ne " ${BOLD}Highpass frequency (Hz, 0=off, 300-2000): ${NC}" + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && VOICE_HIGHPASS=$val + ;; + 6) + echo -ne " ${BOLD}Tremolo speed (Hz, 0=off, 5-40): ${NC}" + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && VOICE_TREMOLO=$val + ;; + 0 | q | Q) + save_config + return + ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } settings_tor() { - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ Tor Settings ═══${NC}\n" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ Tor Settings ═══${NC}\n" - local circ_label="${RED}disabled${NC}" - if [ "$SHOW_CIRCUIT" -eq 1 ]; then - circ_label="${GREEN}enabled${NC}" - fi - local excl_label="${DIM}none${NC}" - if [ -n "$EXCLUDE_NODES" ]; then - excl_label="${YELLOW}${EXCLUDE_NODES}${NC}" - fi - local sf_label="${RED}disabled${NC}" - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - sf_label="${GREEN}enabled${NC}" - fi - echo -e " ${DIM}Circuit display: ${NC}${circ_label}" - echo -e " ${DIM}Exclude nodes: ${NC}${excl_label}" - echo -e " ${DIM}Snowflake bridge: ${NC}${sf_label}" - echo "" + local circ_label="${RED}disabled${NC}" + if [ "$SHOW_CIRCUIT" -eq 1 ]; then + circ_label="${GREEN}enabled${NC}" + fi + local excl_label="${DIM}none${NC}" + if [ -n "$EXCLUDE_NODES" ]; then + excl_label="${YELLOW}${EXCLUDE_NODES}${NC}" + fi + local sf_label="${RED}disabled${NC}" + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + sf_label="${GREEN}enabled${NC}" + fi + echo -e " ${DIM}Circuit display: ${NC}${circ_label}" + echo -e " ${DIM}Exclude nodes: ${NC}${excl_label}" + echo -e " ${DIM}Snowflake bridge: ${NC}${sf_label}" + echo "" - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Toggle circuit hop display in calls" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Exclude countries from circuits" - echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Snowflake bridge (censorship circumvention)" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r _tor_choice + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Toggle circuit hop display in calls" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Exclude countries from circuits" + echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Snowflake bridge (censorship circumvention)" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r _tor_choice - case "$_tor_choice" in - 1) - if [ "$SHOW_CIRCUIT" -eq 1 ]; then - SHOW_CIRCUIT=0 - save_config - log_ok "Circuit display disabled" - else - SHOW_CIRCUIT=1 - save_config - log_ok "Circuit display enabled" - fi - echo -e " ${DIM}Restart Tor for changes to take effect (Main menu → option 10).${NC}" - sleep 2 - ;; - 2) settings_exclude_nodes ;; - 3) settings_snowflake ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + case "$_tor_choice" in + 1) + if [ "$SHOW_CIRCUIT" -eq 1 ]; then + SHOW_CIRCUIT=0 + save_config + log_ok "Circuit display disabled" + else + SHOW_CIRCUIT=1 + save_config + log_ok "Circuit display enabled" + fi + echo -e " ${DIM}Restart Tor for changes to take effect (Main menu → option 10).${NC}" + sleep 2 + ;; + 2) settings_exclude_nodes ;; + 3) settings_snowflake ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } settings_exclude_nodes() { - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ Exclude Countries ═══${NC}\n" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ Exclude Countries ═══${NC}\n" - if [ -n "$EXCLUDE_NODES" ]; then - echo -e " ${DIM}Current:${NC} ${YELLOW}${EXCLUDE_NODES}${NC}" - else - echo -e " ${DIM}Current:${NC} ${DIM}none (all countries allowed)${NC}" - fi - echo -e " ${DIM}Tor will avoid building circuits through excluded countries.${NC}" - echo "" + if [ -n "$EXCLUDE_NODES" ]; then + echo -e " ${DIM}Current:${NC} ${YELLOW}${EXCLUDE_NODES}${NC}" + else + echo -e " ${DIM}Current:${NC} ${DIM}none (all countries allowed)${NC}" + fi + echo -e " ${DIM}Tor will avoid building circuits through excluded countries.${NC}" + echo "" - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Five Eyes ${DIM}(US, GB, CA, AU, NZ)${NC}" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Nine Eyes ${DIM}(+ DK, FR, NL, NO)${NC}" - echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Fourteen Eyes ${DIM}(+ DE, BE, IT, SE, ES)${NC}" - echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Custom countries" - echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} ${RED}Clear (allow all)${NC}" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r _excl_choice + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Five Eyes ${DIM}(US, GB, CA, AU, NZ)${NC}" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Nine Eyes ${DIM}(+ DK, FR, NL, NO)${NC}" + echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Fourteen Eyes ${DIM}(+ DE, BE, IT, SE, ES)${NC}" + echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Custom countries" + echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} ${RED}Clear (allow all)${NC}" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r _excl_choice - case "$_excl_choice" in - 1) - EXCLUDE_NODES="{US},{GB},{CA},{AU},{NZ}" - save_config - log_ok "Excluding Five Eyes countries" - echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" - sleep 2 - ;; - 2) - EXCLUDE_NODES="{US},{GB},{CA},{AU},{NZ},{DK},{FR},{NL},{NO}" - save_config - log_ok "Excluding Nine Eyes countries" - echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" - sleep 2 - ;; - 3) - EXCLUDE_NODES="{US},{GB},{CA},{AU},{NZ},{DK},{FR},{NL},{NO},{DE},{BE},{IT},{SE},{ES}" - save_config - log_ok "Excluding Fourteen Eyes countries" - echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" - sleep 2 - ;; - 4) - clear - echo -e "\n${BOLD}${CYAN}═══ Custom Country Exclusion ═══${NC}\n" - echo -e " ${DIM}Country code reference:${NC}\n" - echo -e " ${WHITE}AF${NC} Afghanistan ${WHITE}AL${NC} Albania ${WHITE}DZ${NC} Algeria" - echo -e " ${WHITE}AR${NC} Argentina ${WHITE}AM${NC} Armenia ${WHITE}AU${NC} Australia" - echo -e " ${WHITE}AT${NC} Austria ${WHITE}AZ${NC} Azerbaijan ${WHITE}BH${NC} Bahrain" - echo -e " ${WHITE}BD${NC} Bangladesh ${WHITE}BY${NC} Belarus ${WHITE}BE${NC} Belgium" - echo -e " ${WHITE}BR${NC} Brazil ${WHITE}BG${NC} Bulgaria ${WHITE}KH${NC} Cambodia" - echo -e " ${WHITE}CA${NC} Canada ${WHITE}CL${NC} Chile ${WHITE}CN${NC} China" - echo -e " ${WHITE}CO${NC} Colombia ${WHITE}HR${NC} Croatia ${WHITE}CU${NC} Cuba" - echo -e " ${WHITE}CY${NC} Cyprus ${WHITE}CZ${NC} Czech Republic ${WHITE}DK${NC} Denmark" - echo -e " ${WHITE}EG${NC} Egypt ${WHITE}EE${NC} Estonia ${WHITE}ET${NC} Ethiopia" - echo -e " ${WHITE}FI${NC} Finland ${WHITE}FR${NC} France ${WHITE}GE${NC} Georgia" - echo -e " ${WHITE}DE${NC} Germany ${WHITE}GR${NC} Greece ${WHITE}HK${NC} Hong Kong" - echo -e " ${WHITE}HU${NC} Hungary ${WHITE}IS${NC} Iceland ${WHITE}IN${NC} India" - echo -e " ${WHITE}ID${NC} Indonesia ${WHITE}IR${NC} Iran ${WHITE}IQ${NC} Iraq" - echo -e " ${WHITE}IE${NC} Ireland ${WHITE}IL${NC} Israel ${WHITE}IT${NC} Italy" - echo -e " ${WHITE}JP${NC} Japan ${WHITE}JO${NC} Jordan ${WHITE}KZ${NC} Kazakhstan" - echo -e " ${WHITE}KE${NC} Kenya ${WHITE}KP${NC} North Korea ${WHITE}KR${NC} South Korea" - echo -e " ${WHITE}KW${NC} Kuwait ${WHITE}LV${NC} Latvia ${WHITE}LB${NC} Lebanon" - echo -e " ${WHITE}LT${NC} Lithuania ${WHITE}LU${NC} Luxembourg ${WHITE}MY${NC} Malaysia" - echo -e " ${WHITE}MX${NC} Mexico ${WHITE}MD${NC} Moldova ${WHITE}MA${NC} Morocco" - echo -e " ${WHITE}NL${NC} Netherlands ${WHITE}NZ${NC} New Zealand ${WHITE}NG${NC} Nigeria" - echo -e " ${WHITE}NO${NC} Norway ${WHITE}PK${NC} Pakistan ${WHITE}PA${NC} Panama" - echo -e " ${WHITE}PH${NC} Philippines ${WHITE}PL${NC} Poland ${WHITE}PT${NC} Portugal" - echo -e " ${WHITE}QA${NC} Qatar ${WHITE}RO${NC} Romania ${WHITE}RU${NC} Russia" - echo -e " ${WHITE}SA${NC} Saudi Arabia ${WHITE}RS${NC} Serbia ${WHITE}SG${NC} Singapore" - echo -e " ${WHITE}SK${NC} Slovakia ${WHITE}SI${NC} Slovenia ${WHITE}ZA${NC} South Africa" - echo -e " ${WHITE}ES${NC} Spain ${WHITE}SE${NC} Sweden ${WHITE}CH${NC} Switzerland" - echo -e " ${WHITE}TW${NC} Taiwan ${WHITE}TH${NC} Thailand ${WHITE}TR${NC} Turkey" - echo -e " ${WHITE}UA${NC} Ukraine ${WHITE}AE${NC} UAE ${WHITE}GB${NC} United Kingdom" - echo -e " ${WHITE}US${NC} United States ${WHITE}UZ${NC} Uzbekistan ${WHITE}VN${NC} Vietnam" - echo "" - echo -e " ${DIM}Enter codes in Tor format, comma-separated.${NC}" - echo -e " ${DIM}Example: {US},{GB},{DE},{RU},{CN}${NC}\n" - echo -ne " ${BOLD}Countries: ${NC}" - read -r _custom_nodes - if [ -n "$_custom_nodes" ]; then - EXCLUDE_NODES="$_custom_nodes" - save_config - log_ok "Excluding: $EXCLUDE_NODES" - else - log_warn "No input — nothing changed" - fi - echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" - sleep 2 - ;; - 5) - EXCLUDE_NODES="" - save_config - log_ok "All countries allowed" - sleep 1 - ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + case "$_excl_choice" in + 1) + EXCLUDE_NODES="{US},{GB},{CA},{AU},{NZ}" + save_config + log_ok "Excluding Five Eyes countries" + echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" + sleep 2 + ;; + 2) + EXCLUDE_NODES="{US},{GB},{CA},{AU},{NZ},{DK},{FR},{NL},{NO}" + save_config + log_ok "Excluding Nine Eyes countries" + echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" + sleep 2 + ;; + 3) + EXCLUDE_NODES="{US},{GB},{CA},{AU},{NZ},{DK},{FR},{NL},{NO},{DE},{BE},{IT},{SE},{ES}" + save_config + log_ok "Excluding Fourteen Eyes countries" + echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" + sleep 2 + ;; + 4) + clear + echo -e "\n${BOLD}${CYAN}═══ Custom Country Exclusion ═══${NC}\n" + echo -e " ${DIM}Country code reference:${NC}\n" + echo -e " ${WHITE}AF${NC} Afghanistan ${WHITE}AL${NC} Albania ${WHITE}DZ${NC} Algeria" + echo -e " ${WHITE}AR${NC} Argentina ${WHITE}AM${NC} Armenia ${WHITE}AU${NC} Australia" + echo -e " ${WHITE}AT${NC} Austria ${WHITE}AZ${NC} Azerbaijan ${WHITE}BH${NC} Bahrain" + echo -e " ${WHITE}BD${NC} Bangladesh ${WHITE}BY${NC} Belarus ${WHITE}BE${NC} Belgium" + echo -e " ${WHITE}BR${NC} Brazil ${WHITE}BG${NC} Bulgaria ${WHITE}KH${NC} Cambodia" + echo -e " ${WHITE}CA${NC} Canada ${WHITE}CL${NC} Chile ${WHITE}CN${NC} China" + echo -e " ${WHITE}CO${NC} Colombia ${WHITE}HR${NC} Croatia ${WHITE}CU${NC} Cuba" + echo -e " ${WHITE}CY${NC} Cyprus ${WHITE}CZ${NC} Czech Republic ${WHITE}DK${NC} Denmark" + echo -e " ${WHITE}EG${NC} Egypt ${WHITE}EE${NC} Estonia ${WHITE}ET${NC} Ethiopia" + echo -e " ${WHITE}FI${NC} Finland ${WHITE}FR${NC} France ${WHITE}GE${NC} Georgia" + echo -e " ${WHITE}DE${NC} Germany ${WHITE}GR${NC} Greece ${WHITE}HK${NC} Hong Kong" + echo -e " ${WHITE}HU${NC} Hungary ${WHITE}IS${NC} Iceland ${WHITE}IN${NC} India" + echo -e " ${WHITE}ID${NC} Indonesia ${WHITE}IR${NC} Iran ${WHITE}IQ${NC} Iraq" + echo -e " ${WHITE}IE${NC} Ireland ${WHITE}IL${NC} Israel ${WHITE}IT${NC} Italy" + echo -e " ${WHITE}JP${NC} Japan ${WHITE}JO${NC} Jordan ${WHITE}KZ${NC} Kazakhstan" + echo -e " ${WHITE}KE${NC} Kenya ${WHITE}KP${NC} North Korea ${WHITE}KR${NC} South Korea" + echo -e " ${WHITE}KW${NC} Kuwait ${WHITE}LV${NC} Latvia ${WHITE}LB${NC} Lebanon" + echo -e " ${WHITE}LT${NC} Lithuania ${WHITE}LU${NC} Luxembourg ${WHITE}MY${NC} Malaysia" + echo -e " ${WHITE}MX${NC} Mexico ${WHITE}MD${NC} Moldova ${WHITE}MA${NC} Morocco" + echo -e " ${WHITE}NL${NC} Netherlands ${WHITE}NZ${NC} New Zealand ${WHITE}NG${NC} Nigeria" + echo -e " ${WHITE}NO${NC} Norway ${WHITE}PK${NC} Pakistan ${WHITE}PA${NC} Panama" + echo -e " ${WHITE}PH${NC} Philippines ${WHITE}PL${NC} Poland ${WHITE}PT${NC} Portugal" + echo -e " ${WHITE}QA${NC} Qatar ${WHITE}RO${NC} Romania ${WHITE}RU${NC} Russia" + echo -e " ${WHITE}SA${NC} Saudi Arabia ${WHITE}RS${NC} Serbia ${WHITE}SG${NC} Singapore" + echo -e " ${WHITE}SK${NC} Slovakia ${WHITE}SI${NC} Slovenia ${WHITE}ZA${NC} South Africa" + echo -e " ${WHITE}ES${NC} Spain ${WHITE}SE${NC} Sweden ${WHITE}CH${NC} Switzerland" + echo -e " ${WHITE}TW${NC} Taiwan ${WHITE}TH${NC} Thailand ${WHITE}TR${NC} Turkey" + echo -e " ${WHITE}UA${NC} Ukraine ${WHITE}AE${NC} UAE ${WHITE}GB${NC} United Kingdom" + echo -e " ${WHITE}US${NC} United States ${WHITE}UZ${NC} Uzbekistan ${WHITE}VN${NC} Vietnam" + echo "" + echo -e " ${DIM}Enter codes in Tor format, comma-separated.${NC}" + echo -e " ${DIM}Example: {US},{GB},{DE},{RU},{CN}${NC}\n" + echo -ne " ${BOLD}Countries: ${NC}" + read -r _custom_nodes + if [ -n "$_custom_nodes" ]; then + EXCLUDE_NODES="$_custom_nodes" + save_config + log_ok "Excluding: $EXCLUDE_NODES" + else + log_warn "No input — nothing changed" + fi + echo -e " ${DIM}Restart Tor for changes to take effect.${NC}" + sleep 2 + ;; + 5) + EXCLUDE_NODES="" + save_config + log_ok "All countries allowed" + sleep 1 + ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } settings_security() { - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ Security ═══${NC}\n" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ Security ═══${NC}\n" - local hmac_label="${RED}disabled${NC}" - if [ "$HMAC_AUTH" -eq 1 ]; then - hmac_label="${GREEN}enabled${NC}" - fi - local cipher_upper="${CIPHER^^}" - echo -e " ${DIM}Cipher: ${NC}${WHITE}${cipher_upper}${NC}" - echo -e " ${DIM}HMAC auth: ${NC}${hmac_label}" - echo "" + local hmac_label="${RED}disabled${NC}" + if [ "$HMAC_AUTH" -eq 1 ]; then + hmac_label="${GREEN}enabled${NC}" + fi + local cipher_upper="${CIPHER^^}" + echo -e " ${DIM}Cipher: ${NC}${WHITE}${cipher_upper}${NC}" + echo -e " ${DIM}HMAC auth: ${NC}${hmac_label}" + echo "" - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Change encryption cipher" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} HMAC authentication" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r _sec_choice + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Change encryption cipher" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} HMAC authentication" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r _sec_choice - case "$_sec_choice" in - 1) settings_cipher ;; - 2) settings_hmac ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + case "$_sec_choice" in + 1) settings_cipher ;; + 2) settings_hmac ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } settings_hmac() { - while true; do - clear - echo -e "\n${BOLD}${CYAN}═══ HMAC Authentication ═══${NC}\n" + while true; do + clear + echo -e "\n${BOLD}${CYAN}═══ HMAC Authentication ═══${NC}\n" - local hmac_label="${RED}disabled${NC}" - if [ "$HMAC_AUTH" -eq 1 ]; then - hmac_label="${GREEN}enabled${NC}" + local hmac_label="${RED}disabled${NC}" + if [ "$HMAC_AUTH" -eq 1 ]; then + hmac_label="${GREEN}enabled${NC}" + fi + echo -e " ${DIM}Status:${NC} ${hmac_label}" + echo "" + + echo -e " ${DIM}When enabled, every message sent during a call (voice,${NC}" + echo -e " ${DIM}text, hangup, and all control signals) is signed with${NC}" + echo -e " ${DIM}HMAC-SHA256 derived from your shared secret.${NC}" + echo "" + echo -e " ${DIM}A random nonce is included with each message so that${NC}" + echo -e " ${DIM}identical commands produce a unique signature every time.${NC}" + echo -e " ${DIM}This prevents replay attacks — a captured message cannot${NC}" + echo -e " ${DIM}be re-sent to disrupt future calls.${NC}" + echo "" + echo -e " ${DIM}On the receiving end, any message with an invalid or${NC}" + echo -e " ${DIM}missing signature is silently dropped. An attacker who${NC}" + echo -e " ${DIM}compromises the Tor circuit but does not have the shared${NC}" + echo -e " ${DIM}secret cannot inject commands like HANGUP to disconnect${NC}" + echo -e " ${DIM}your call or forge audio and text messages.${NC}" + echo "" + echo -e " ${YELLOW}Both parties must have HMAC enabled for calls to work.${NC}" + echo -e " ${YELLOW}Not compatible with versions prior to 1.1.3.${NC}" + echo "" + + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Turn on" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Turn off" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" + read -r _hmac_choice + + case "$_hmac_choice" in + 1) + if [ "$HMAC_AUTH" -eq 1 ]; then + log_info "HMAC authentication is already enabled" + else + HMAC_AUTH=1 + save_config + log_ok "HMAC authentication enabled" + if [ "$CALL_ACTIVE" -eq 1 ]; then + echo -e " ${YELLOW}Takes effect on the next call.${NC}" fi - echo -e " ${DIM}Status:${NC} ${hmac_label}" - echo "" - - echo -e " ${DIM}When enabled, every message sent during a call (voice,${NC}" - echo -e " ${DIM}text, hangup, and all control signals) is signed with${NC}" - echo -e " ${DIM}HMAC-SHA256 derived from your shared secret.${NC}" - echo "" - echo -e " ${DIM}A random nonce is included with each message so that${NC}" - echo -e " ${DIM}identical commands produce a unique signature every time.${NC}" - echo -e " ${DIM}This prevents replay attacks — a captured message cannot${NC}" - echo -e " ${DIM}be re-sent to disrupt future calls.${NC}" - echo "" - echo -e " ${DIM}On the receiving end, any message with an invalid or${NC}" - echo -e " ${DIM}missing signature is silently dropped. An attacker who${NC}" - echo -e " ${DIM}compromises the Tor circuit but does not have the shared${NC}" - echo -e " ${DIM}secret cannot inject commands like HANGUP to disconnect${NC}" - echo -e " ${DIM}your call or forge audio and text messages.${NC}" - echo "" - echo -e " ${YELLOW}Both parties must have HMAC enabled for calls to work.${NC}" - echo -e " ${YELLOW}Not compatible with versions prior to 1.1.3.${NC}" - echo "" - - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Turn on" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Turn off" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" - read -r _hmac_choice - - case "$_hmac_choice" in - 1) - if [ "$HMAC_AUTH" -eq 1 ]; then - log_info "HMAC authentication is already enabled" - else - HMAC_AUTH=1 - save_config - log_ok "HMAC authentication enabled" - if [ "$CALL_ACTIVE" -eq 1 ]; then - echo -e " ${YELLOW}Takes effect on the next call.${NC}" - fi - fi - sleep 1 - ;; - 2) - if [ "$HMAC_AUTH" -eq 0 ]; then - log_info "HMAC authentication is already disabled" - else - HMAC_AUTH=0 - save_config - log_ok "HMAC authentication disabled" - if [ "$CALL_ACTIVE" -eq 1 ]; then - echo -e " ${YELLOW}Takes effect on the next call.${NC}" - fi - fi - sleep 1 - ;; - 0|q|Q) return ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + fi + sleep 1 + ;; + 2) + if [ "$HMAC_AUTH" -eq 0 ]; then + log_info "HMAC authentication is already disabled" + else + HMAC_AUTH=0 + save_config + log_ok "HMAC authentication disabled" + if [ "$CALL_ACTIVE" -eq 1 ]; then + echo -e " ${YELLOW}Takes effect on the next call.${NC}" + fi + fi + sleep 1 + ;; + 0 | q | Q) return ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } #============================================================================= @@ -2820,196 +2821,199 @@ settings_hmac() { #============================================================================= show_banner() { - clear - echo "" - echo -e "${BOLD}${TOR_PURPLE} ╔╦╗┌─┐┬─┐┌┬┐┬┌┐┌┌─┐┬ ╔═╗┬ ┬┌─┐┌┐┌┌─┐${NC}" - echo -e "${BOLD}${TOR_PURPLE} ║ ├┤ ├┬┘│││││││├─┤│ ╠═╝├─┤│ ││││├┤ ${NC}" - echo -e "${BOLD}${TOR_PURPLE} ╩ └─┘┴└─┴ ┴┴┘└┘┴ ┴┴─┘╩ ┴ ┴└─┘┘└┘└─┘${NC}" - echo "" - echo -e " ${TOR_PURPLE}───────────────────────────────────────${NC}" - echo -e " ${TOR_PURPLE}${BOLD}Encrypted Voice & Chat${NC} ${DIM}over${NC} ${TOR_PURPLE}${BOLD}Tor${NC} ${DIM}Hidden Services${NC}" - echo -e " ${TOR_PURPLE}───────────────────────────────────────${NC}" - local cipher_display="${CIPHER^^}" - echo -e " ${DIM}v${VERSION} | Push-to-Talk | End-to-End ${cipher_display}${NC}\n" + clear + echo "" + echo -e "${BOLD}${TOR_PURPLE} ╔╦╗┌─┐┬─┐┌┬┐┬┌┐┌┌─┐┬ ╔═╗┬ ┬┌─┐┌┐┌┌─┐${NC}" + echo -e "${BOLD}${TOR_PURPLE} ║ ├┤ ├┬┘│││││││├─┤│ ╠═╝├─┤│ ││││├┤ ${NC}" + echo -e "${BOLD}${TOR_PURPLE} ╩ └─┘┴└─┴ ┴┴┘└┘┴ ┴┴─┘╩ ┴ ┴└─┘┘└┘└─┘${NC}" + echo "" + echo -e " ${TOR_PURPLE}───────────────────────────────────────${NC}" + echo -e " ${TOR_PURPLE}${BOLD}Encrypted Voice & Chat${NC} ${DIM}over${NC} ${TOR_PURPLE}${BOLD}Tor${NC} ${DIM}Hidden Services${NC}" + echo -e " ${TOR_PURPLE}───────────────────────────────────────${NC}" + local cipher_display="${CIPHER^^}" + echo -e " ${DIM}v${VERSION} | Push-to-Talk | End-to-End ${cipher_display}${NC}\n" } main_menu() { - while true; do - show_banner + while true; do + show_banner - # Show quick status line - local tor_status="${RED}●${NC}" - if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then - tor_status="${GREEN}●${NC}" - fi - local secret_status="${RED}●${NC}" - if [ -n "$SHARED_SECRET" ]; then - secret_status="${GREEN}●${NC}" - fi - local sf_status="${RED}●${NC}" - if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then - sf_status="${GREEN}●${NC}" - fi - local al_status="${RED}●${NC}" - if [ "$AUTO_LISTEN" -eq 1 ]; then - al_status="${GREEN}●${NC}" - fi - local _ptt_d="SPACE" - [ "$PTT_KEY" != " " ] && _ptt_d="$PTT_KEY" + # Show quick status line + local tor_status="${RED}●${NC}" + if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then + tor_status="${GREEN}●${NC}" + fi + local secret_status="${RED}●${NC}" + if [ -n "$SHARED_SECRET" ]; then + secret_status="${GREEN}●${NC}" + fi + local sf_status="${RED}●${NC}" + if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then + sf_status="${GREEN}●${NC}" + fi + local al_status="${RED}●${NC}" + if [ "$AUTO_LISTEN" -eq 1 ]; then + al_status="${GREEN}●${NC}" + fi + local _ptt_d="SPACE" + [ "$PTT_KEY" != " " ] && _ptt_d="$PTT_KEY" - echo -e " ${DIM}Tor:${NC} $tor_status ${DIM}Secret:${NC} $secret_status ${DIM}SF:${NC} $sf_status ${DIM}AL:${NC} $al_status ${DIM}PTT:${NC} ${GREEN}[${_ptt_d}]${NC}\n" + echo -e " ${DIM}Tor:${NC} $tor_status ${DIM}Secret:${NC} $secret_status ${DIM}SF:${NC} $sf_status ${DIM}AL:${NC} $al_status ${DIM}PTT:${NC} ${GREEN}[${_ptt_d}]${NC}\n" - echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Listen for calls" - echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Call an onion address" - echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Show my onion address" - echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Set shared secret" - echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Test audio (loopback)" - echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Show status" - echo -e " ${BOLD}${WHITE}7${NC} ${CYAN}│${NC} Install dependencies" - echo -e " ${BOLD}${WHITE}8${NC} ${CYAN}│${NC} Start Tor" - echo -e " ${BOLD}${WHITE}9${NC} ${CYAN}│${NC} Stop Tor" - echo -e " ${BOLD}${WHITE}10${NC}${CYAN}│${NC} Restart Tor" - echo -e " ${BOLD}${WHITE}11${NC}${CYAN}│${NC} Rotate onion address" - echo -e " ${BOLD}${WHITE}12${NC}${CYAN}│${NC} Settings" - echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${RED}Quit${NC}" - echo "" - echo -ne " ${BOLD}Select: ${NC}" + echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Listen for calls" + echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Call an onion address" + echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Show my onion address" + echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Set shared secret" + echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Test audio (loopback)" + echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Show status" + echo -e " ${BOLD}${WHITE}7${NC} ${CYAN}│${NC} Install dependencies" + echo -e " ${BOLD}${WHITE}8${NC} ${CYAN}│${NC} Start Tor" + echo -e " ${BOLD}${WHITE}9${NC} ${CYAN}│${NC} Stop Tor" + echo -e " ${BOLD}${WHITE}10${NC}${CYAN}│${NC} Restart Tor" + echo -e " ${BOLD}${WHITE}11${NC}${CYAN}│${NC} Rotate onion address" + echo -e " ${BOLD}${WHITE}12${NC}${CYAN}│${NC} Settings" + echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${RED}Quit${NC}" + echo "" + echo -ne " ${BOLD}Select: ${NC}" - # If auto-listen is active, poll for incoming calls without redrawing - local choice="" - if [ "$AUTO_LISTEN" -eq 1 ] && [ -n "$AUTO_LISTEN_PID" ]; then - echo -ne "${DIM}[Auto-listening...]${NC} " >&2 - while true; do - # Check for incoming call - if check_auto_listen; then - choice="" - break - fi - # Try to read user input with short timeout - if read -r -t 1 choice 2>/dev/null; then - break # user typed something - fi - done + # If auto-listen is active, poll for incoming calls without redrawing + local choice="" + if [ "$AUTO_LISTEN" -eq 1 ] && [ -n "$AUTO_LISTEN_PID" ]; then + echo -ne "${DIM}[Auto-listening...]${NC} " >&2 + while true; do + # Check for incoming call + if check_auto_listen; then + choice="" + break + fi + # Try to read user input with short timeout + if read -r -t 1 choice 2>/dev/null; then + break # user typed something + fi + done + else + read -r choice + fi + + [ -z "$choice" ] && continue + + case "$choice" in + 1) listen_for_call ;; + 2) call_remote ;; + 3) + local onion + onion=$(get_onion) + if [ -n "$onion" ]; then + echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n" + + # QR code generation + if check_dep qrencode; then + echo -ne " ${BOLD}Show QR code? [Y/n]: ${NC}" + read -r _qr_show + if [ "$_qr_show" != "n" ] && [ "$_qr_show" != "N" ]; then + tput smcup 2>/dev/null || true + clear + echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n" + qrencode -t ANSIUTF8 "$onion" + echo "" + echo -ne " ${DIM}Press Enter to dismiss QR code...${NC}" + read -r + tput rmcup 2>/dev/null || true + fi else - read -r choice + echo -ne " ${BOLD}Install QR code generator (qrencode) to display a scannable QR? [Y/n]: ${NC}" + read -r _qr_install + if [ "$_qr_install" != "n" ] && [ "$_qr_install" != "N" ]; then + local SUDO="sudo" + if [ $IS_TERMUX -eq 1 ]; then + SUDO="" + pkg install -y libqrencode 2>/dev/null + elif check_dep apt-get; then + $SUDO apt-get install -y qrencode 2>/dev/null + elif check_dep dnf; then + $SUDO dnf install -y qrencode 2>/dev/null + elif check_dep pacman; then + $SUDO pacman -S --noconfirm qrencode 2>/dev/null + else + log_err "No supported package manager found. Install qrencode manually." + fi + + if check_dep qrencode; then + log_ok "qrencode installed successfully!" + sleep 1 + tput smcup 2>/dev/null || true + clear + echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n" + qrencode -t ANSIUTF8 "$onion" + echo "" + echo -ne " ${DIM}Press Enter to dismiss QR code...${NC}" + read -r + tput rmcup 2>/dev/null || true + else + log_err "qrencode installation failed." + fi + fi fi - - [ -z "$choice" ] && continue - - case "$choice" in - 1) listen_for_call ;; - 2) call_remote ;; - 3) - local onion - onion=$(get_onion) - if [ -n "$onion" ]; then - echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n" - - # QR code generation - if check_dep qrencode; then - echo -ne " ${BOLD}Show QR code? [Y/n]: ${NC}" - read -r _qr_show - if [ "$_qr_show" != "n" ] && [ "$_qr_show" != "N" ]; then - tput smcup 2>/dev/null || true - clear - echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n" - qrencode -t ANSIUTF8 "$onion" - echo "" - echo -ne " ${DIM}Press Enter to dismiss QR code...${NC}" - read -r - tput rmcup 2>/dev/null || true - fi - else - echo -ne " ${BOLD}Install QR code generator (qrencode) to display a scannable QR? [Y/n]: ${NC}" - read -r _qr_install - if [ "$_qr_install" != "n" ] && [ "$_qr_install" != "N" ]; then - local SUDO="sudo" - if [ $IS_TERMUX -eq 1 ]; then - SUDO="" - pkg install -y libqrencode 2>/dev/null - elif check_dep apt-get; then - $SUDO apt-get install -y qrencode 2>/dev/null - elif check_dep dnf; then - $SUDO dnf install -y qrencode 2>/dev/null - elif check_dep pacman; then - $SUDO pacman -S --noconfirm qrencode 2>/dev/null - else - log_err "No supported package manager found. Install qrencode manually." - fi - - if check_dep qrencode; then - log_ok "qrencode installed successfully!" - sleep 1 - tput smcup 2>/dev/null || true - clear - echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n" - qrencode -t ANSIUTF8 "$onion" - echo "" - echo -ne " ${DIM}Press Enter to dismiss QR code...${NC}" - read -r - tput rmcup 2>/dev/null || true - else - log_err "qrencode installation failed." - fi - fi - fi - else - echo -e "\n ${YELLOW}Tor hidden service not running. Start Tor first (option 8).${NC}\n" - fi - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 4) set_shared_secret ;; - 5) test_audio - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 6) show_status - echo -ne " ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 7) install_deps - echo -ne "\n ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 8) - start_tor - start_auto_listener - echo -ne "\n ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 9) - stop_tor - echo -ne "\n ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 10) - stop_tor - start_tor - start_auto_listener - echo -ne "\n ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 11) - rotate_onion - echo -ne "\n ${DIM}Press Enter to continue...${NC}" - read -r - ;; - 12) - settings_menu - ;; - 0|q|Q) - echo -e "\n${GREEN}Goodbye!${NC}" - stop_tor - cleanup - exit 0 - ;; - *) - echo -e "\n ${RED}Invalid choice${NC}" - sleep 1 - ;; - esac - done + else + echo -e "\n ${YELLOW}Tor hidden service not running. Start Tor first (option 8).${NC}\n" + fi + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 4) set_shared_secret ;; + 5) + test_audio + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 6) + show_status + echo -ne " ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 7) + install_deps + echo -ne "\n ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 8) + start_tor + start_auto_listener + echo -ne "\n ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 9) + stop_tor + echo -ne "\n ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 10) + stop_tor + start_tor + start_auto_listener + echo -ne "\n ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 11) + rotate_onion + echo -ne "\n ${DIM}Press Enter to continue...${NC}" + read -r + ;; + 12) + settings_menu + ;; + 0 | q | Q) + echo -e "\n${GREEN}Goodbye!${NC}" + stop_tor + cleanup + exit 0 + ;; + *) + echo -e "\n ${RED}Invalid choice${NC}" + sleep 1 + ;; + esac + done } #============================================================================= @@ -3029,47 +3033,47 @@ load_config # Handle command-line arguments case "${1:-}" in - install) - install_deps - ;; - test) - test_audio - ;; - status) - show_status - ;; - listen) - load_config - listen_for_call - ;; - call) - load_config - if [ -n "${2:-}" ]; then - remote_onion="$2" - if [[ "$remote_onion" != *.onion ]]; then - remote_onion="${remote_onion}.onion" - fi - start_tor - call_remote - else - echo "Usage: $0 call " - fi - ;; - help|-h|--help) - echo -e "${BOLD}${APP_NAME} v${VERSION}${NC}" - echo "" - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " (none) Interactive menu" - echo " install Install dependencies" - echo " test Run audio loopback test" - echo " status Show current status" - echo " listen Start listening for calls" - echo " call ADDR Call an onion address" - echo " help Show this help" - ;; - *) - main_menu - ;; +install) + install_deps + ;; +test) + test_audio + ;; +status) + show_status + ;; +listen) + load_config + listen_for_call + ;; +call) + load_config + if [ -n "${2:-}" ]; then + remote_onion="$2" + if [[ "$remote_onion" != *.onion ]]; then + remote_onion="${remote_onion}.onion" + fi + start_tor + call_remote + else + echo "Usage: $0 call " + fi + ;; +help | -h | --help) + echo -e "${BOLD}${APP_NAME} v${VERSION}${NC}" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " (none) Interactive menu" + echo " install Install dependencies" + echo " test Run audio loopback test" + echo " status Show current status" + echo " listen Start listening for calls" + echo " call ADDR Call an onion address" + echo " help Show this help" + ;; +*) + main_menu + ;; esac