#!/usr/bin/env bash # TerminalPhone — Encrypted Push-to-Talk Voice over Tor # A walkie-talkie style voice chat using Tor hidden services # License: MIT set -euo pipefail #============================================================================= # CONFIGURATION #============================================================================= APP_NAME="TerminalPhone" VERSION="1.0.2" BASE_DIR="$(dirname "$(readlink -f "$0")")" DATA_DIR="$BASE_DIR/.terminalphone" TOR_DIR="$DATA_DIR/tor_data" TOR_CONF="$DATA_DIR/torrc" ONION_FILE="$TOR_DIR/hidden_service/hostname" SECRET_FILE="$DATA_DIR/shared_secret" CONFIG_FILE="$DATA_DIR/config" AUDIO_DIR="$DATA_DIR/audio" PID_DIR="$DATA_DIR/pids" PTT_FLAG="$DATA_DIR/run/ptt_$$" CONNECTED_FLAG="$DATA_DIR/run/connected_$$" RECV_PIPE="$DATA_DIR/run/recv_$$" SEND_PIPE="$DATA_DIR/run/send_$$" CIPHER_RUNTIME_FILE="$DATA_DIR/run/cipher_$$" # Defaults LISTEN_PORT=7777 TOR_SOCKS_PORT=9050 OPUS_BITRATE=16 # kbps — good balance of quality and bandwidth for Tor OPUS_FRAMESIZE=60 # ms SAMPLE_RATE=8000 # Hz PTT_KEY=" " # spacebar CHUNK_DURATION=1 # seconds per audio chunk CIPHER="aes-256-cbc" # OpenSSL cipher for encryption # ANSI Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' DIM='\033[2m' BOLD='\033[1m' BLINK='\033[5m' NC='\033[0m' # No Color BG_RED='\033[41m' BG_GREEN='\033[42m' 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 fi # State TOR_PID="" CALL_ACTIVE=0 ORIGINAL_STTY="" #============================================================================= # HELPERS #============================================================================= cleanup() { # 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 # 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}" } 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 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 } save_pid() { local name="$1" pid="$2" mkdir -p "$PID_DIR" echo "$pid" > "$PID_DIR/${name}.pid" } log_info() { echo -e "${CYAN}[INFO]${NC} $1" } log_ok() { echo -e "${GREEN}[ OK]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_err() { echo -e "${RED}[FAIL]${NC} $1" } uid() { echo "$(date +%s%N 2>/dev/null || date +%s)_${RANDOM}" } load_config() { if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE" fi if [ -f "$SECRET_FILE" ]; then SHARED_SECRET=$(cat "$SECRET_FILE") else SHARED_SECRET="" fi } save_config() { mkdir -p "$DATA_DIR" cat > "$CONFIG_FILE" << EOF LISTEN_PORT=$LISTEN_PORT TOR_SOCKS_PORT=$TOR_SOCKS_PORT OPUS_BITRATE=$OPUS_BITRATE OPUS_FRAMESIZE=$OPUS_FRAMESIZE PTT_KEY="$PTT_KEY" CIPHER="$CIPHER" EOF } #============================================================================= # DEPENDENCY INSTALLER #============================================================================= check_dep() { command -v "$1" &>/dev/null } install_deps() { 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" # 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 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 echo -e "\n${YELLOW}Missing dependencies: ${deps_needed[*]}${NC}" echo -e "Attempting to install...\n" # 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 # 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 } #============================================================================= # TOR HIDDEN SERVICE #============================================================================= setup_tor() { mkdir -p "$TOR_DIR/hidden_service" chmod 700 "$TOR_DIR/hidden_service" cat > "$TOR_CONF" << EOF SocksPort $TOR_SOCKS_PORT DataDirectory $TOR_DIR/data HiddenServiceDir $TOR_DIR/hidden_service HiddenServicePort $LISTEN_PORT 127.0.0.1:$LISTEN_PORT Log notice file $TOR_DIR/tor.log EOF chmod 600 "$TOR_CONF" } start_tor() { 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..." 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 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() { 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" fi } get_onion() { 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 } #============================================================================= # ENCRYPTION #============================================================================= 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" 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 if [ -z "$new_secret" ]; then log_warn "Secret not changed" return fi SHARED_SECRET="$new_secret" mkdir -p "$DATA_DIR" echo "$SHARED_SECRET" > "$SECRET_FILE" chmod 600 "$SECRET_FILE" log_ok "Shared secret saved" } # Encrypt stdin to stdout encrypt_stream() { local c="$CIPHER" [ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE") openssl enc -"${c}" -pbkdf2 -iter 10000 -pass "pass:${SHARED_SECRET}" -nosalt 2>/dev/null } # Decrypt stdin to stdout decrypt_stream() { local c="$CIPHER" [ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE") openssl enc -d -"${c}" -pbkdf2 -iter 10000 -pass "pass:${SHARED_SECRET}" -nosalt 2>/dev/null } # 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 "pass:${SHARED_SECRET}" \ -in "$infile" -out "$outfile" 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 "pass:${SHARED_SECRET}" \ -in "$infile" -out "$outfile" 2>/dev/null } #============================================================================= # AUDIO PIPELINE #============================================================================= # Record a timed chunk of raw audio (used by audio test) audio_record() { local outfile="$1" local duration="${2:-$CHUNK_DURATION}" if [ $IS_TERMUX -eq 1 ]; then local tmp_rec="$AUDIO_DIR/tmrec_$(uid).m4a" 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) if [ $IS_TERMUX -eq 1 ]; then REC_FILE="$AUDIO_DIR/msg_${_id}.m4a" rm -f "$REC_FILE" termux-microphone-record -l 120 -f "$REC_FILE" &>/dev/null & REC_PID=$! else REC_FILE="$AUDIO_DIR/msg_${_id}.raw" arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -q "$REC_FILE" 2>/dev/null & REC_PID=$! fi } # 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}.raw" local opus_file="$AUDIO_DIR/tx_${_id}.opus" local enc_file="$AUDIO_DIR/tx_enc_${_id}.opus" # 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="" # Encode → encrypt → send if [ -s "$raw_file" ]; then echo -ne "\r ${DIM}Sending...${NC} " >&2 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 b64 b64=$(base64 -w 0 "$enc_file" 2>/dev/null) echo "AUDIO:${b64}" >&4 2>/dev/null || true fi fi 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}" if [ $IS_TERMUX -eq 1 ]; then # Termux: use Android's native media player termux-media-player play "$infile" &>/dev/null || true # Wait for playback to finish while termux-media-player info 2>/dev/null | grep -q "playing"; do sleep 0.5 done 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" if [ $IS_TERMUX -eq 1 ]; then # Termux: decode to wav, play via Android media player local wav_file="$AUDIO_DIR/play_$(uid).wav" opusdec --quiet "$opus_file" "$wav_file" 2>/dev/null || true if [ -s "$wav_file" ]; then audio_play "$wav_file" fi rm -f "$wav_file" 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 fi stty sane 2>/dev/null || true ORIGINAL_STTY="" # Close pipe file descriptors to unblock any blocking reads 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 # 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 "$DATA_DIR/run/remote_id_$$" rm -f "$DATA_DIR/run/remote_cipher_$$" rm -f "$DATA_DIR/run/incoming_$$" # Clean temp audio files rm -f "$AUDIO_DIR"/*.opus "$AUDIO_DIR"/*.bin "$AUDIO_DIR"/*.txt "$AUDIO_DIR"/*.wav 2>/dev/null || true # Reset state variables CALL_ACTIVE=0 REC_PID="" } # Start listening for incoming calls listen_for_call() { if [ -z "$SHARED_SECRET" ]; then log_err "No shared secret set! Use option 4 first." return 1 fi start_tor || return 1 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}Press Ctrl+C to stop listening.${NC}\n" mkdir -p "$AUDIO_DIR" # Use socat to accept a TCP connection, then handle it log_info "Waiting for incoming connection..." # Create named pipes for bidirectional communication rm -f "$RECV_PIPE" "$SEND_PIPE" mkfifo "$RECV_PIPE" "$SEND_PIPE" # Flag file that socat will create when a connection arrives local incoming_flag="$DATA_DIR/run/incoming_$$" rm -f "$incoming_flag" # Start socat in background — it touches the flag when someone connects socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \ "SYSTEM:touch $incoming_flag; cat $SEND_PIPE & cat > $RECV_PIPE" & local socat_pid=$! save_pid "socat" "$socat_pid" # Wait for an incoming connection (poll for the flag file) 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" return 1 fi sleep 0.5 done touch "$CONNECTED_FLAG" log_ok "Call connected!" in_call_session "$RECV_PIPE" "$SEND_PIPE" "" # Full cleanup after call ends cleanup_call } # 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 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 # Append .onion if not present if [[ "$remote_onion" != *.onion ]]; then remote_onion="${remote_onion}.onion" fi start_tor || return 1 echo -e "\n ${DIM}Connecting to ${remote_onion}:${LISTEN_PORT} via Tor...${NC}" mkdir -p "$AUDIO_DIR" touch "$CONNECTED_FLAG" # 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" # Give socat a moment to connect sleep 2 if kill -0 "$socat_pid" 2>/dev/null; then log_ok "Connected to ${remote_onion}!" in_call_session "$RECV_PIPE" "$SEND_PIPE" "$remote_onion" else log_err "Failed to connect. Check the address and ensure Tor is running." fi # Full cleanup after call ends cleanup_call } #============================================================================= # IN-CALL SESSION — PTT VOICE LOOP #============================================================================= # Draw the call header (reusable for redraw after settings) draw_call_header() { local _remote="${1:-}" local _rcipher="${2:-}" clear >&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 # Show cipher info — always show both local and remote 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 " ${RED}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2 echo -e " ${RED}●${NC} Remote cipher: ${WHITE}${rcipher_upper}${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 fi echo "" >&2 echo -e " ${BOLD}Controls:${NC}" >&2 echo -e " ${GREEN}[SPACE]${NC} -- Push-to-Talk" >&2 echo -e " ${CYAN}[T]${NC} -- Send text message" >&2 echo -e " ${YELLOW}[S]${NC} -- Settings" >&2 echo -e " ${RED}[Q]${NC} -- Hang up" >&2 echo -e "" >&2 } in_call_session() { local recv_pipe="$1" local send_pipe="$2" local known_remote="${3:-}" CALL_ACTIVE=1 rm -f "$PTT_FLAG" mkdir -p "$AUDIO_DIR" # Write cipher to runtime file so subshells can track changes echo "$CIPHER" > "$CIPHER_RUNTIME_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 # Send our onion address and cipher for handshake local my_onion my_onion=$(get_onion) if [ -n "$my_onion" ]; then echo "ID:${my_onion}" >&4 2>/dev/null || true fi echo "CIPHER:${CIPHER}" >&4 2>/dev/null || true # 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 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 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 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 # Draw call header draw_call_header "$remote_display" "$remote_cipher" # 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 case "$line" in PTT_START) echo -ne "\r ${BG_GREEN}${WHITE}${BOLD} REMOTE TALKING ${NC} " ;; PTT_STOP) echo -ne "\r ${DIM} Remote idle ${NC} " ;; PING) echo -ne "\r ${DIM} Ping received ${NC} " ;; 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[4;1H\033[K' >&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}.bin" local msg_dec="$AUDIO_DIR/msg_dec_${_mid}.txt" 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 "\r\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}.opus" local dec_file="$AUDIO_DIR/recv_dec_${_rid}.opus" 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 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="" local ptt_active=0 if [ $IS_TERMUX -eq 1 ]; then echo -ne "\r ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 else echo -ne "\r ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 fi while [ -f "$CONNECTED_FLAG" ]; do local key="" key=$(dd bs=1 count=1 2>/dev/null) || true if [ "$key" = " " ]; then if [ $IS_TERMUX -eq 1 ]; then # TERMUX: Toggle mode if [ $ptt_active -eq 0 ]; then ptt_active=1 echo -ne "\r ${BG_RED}${WHITE}${BOLD} ● RECORDING ${NC} ${DIM}[SPACE]=Send${NC} " >&2 start_recording else ptt_active=0 stop_and_send echo "PTT_STOP" >&4 2>/dev/null || true echo -ne "\r ${GREEN}${BOLD} Sent! ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2 fi else # LINUX: Hold-to-talk if [ $ptt_active -eq 0 ]; then ptt_active=1 echo -ne "\r ${BG_RED}${WHITE}${BOLD}${BLINK} ● RECORDING ${NC} " >&2 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 echo "HANGUP" >&4 2>/dev/null || true 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 ptt_active=0 stop_and_send echo "PTT_STOP" >&4 2>/dev/null || true echo -ne "\r ${GREEN}${BOLD} Sent! ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&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 -e "\r " >&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}.txt" local chat_enc="$AUDIO_DIR/chat_enc_${_cid}.bin" 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) echo "MSG:${chat_b64}" >&4 2>/dev/null || true 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 echo "" >&2 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 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 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 fi done echo -e "\n${BOLD}${RED} CALL ENDED ${NC}\n" } #============================================================================= # AUDIO TEST (LOOPBACK) #============================================================================= test_audio() { 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) 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}.raw" 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}.opus" 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}.opus" local dec_file="$AUDIO_DIR/test_dec_${_tid}.opus" 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}.opus" 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" } #============================================================================= # SHOW STATUS #============================================================================= show_status() { 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" fi # 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 # 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 "" } #============================================================================= # SETTINGS MENU #============================================================================= settings_menu() { while true; do clear echo -e "\n${BOLD}${CYAN}═══ Settings ═══${NC}\n" echo -e " ${DIM}Current cipher: ${NC}${WHITE}${CIPHER}${NC}" echo -e " ${DIM}Current Opus bitrate: ${NC}${WHITE}${OPUS_BITRATE} kbps${NC}" echo -e " ${DIM}Current Opus frame: ${NC}${WHITE}${OPUS_FRAMESIZE} ms${NC}\n" echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Change encryption cipher" echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Change Opus encoding quality" 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_cipher ;; 2) settings_opus ;; 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" # 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[@]} 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]}" # 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 -nosalt 2>/dev/null | openssl enc -d -"${selected}" -pbkdf2 -pass pass:test -nosalt &>/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 echo "CIPHER:${CIPHER}" >&4 2>/dev/null || true 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" 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 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 } #============================================================================= # MAIN MENU #============================================================================= 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" } main_menu() { 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 echo -e " ${DIM}Tor:${NC} $tor_status ${DIM}Secret:${NC} $secret_status ${DIM}PTT:${NC} ${GREEN}[SPACE]${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}" read -r choice 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" 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 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 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 } #============================================================================= # ENTRY POINT #============================================================================= trap cleanup EXIT INT TERM # Create data directories mkdir -p "$DATA_DIR" "$AUDIO_DIR" "$PID_DIR" "$DATA_DIR/run" # Clean any stale run files from previous sessions rm -f "$DATA_DIR/run/"* 2>/dev/null || true # Load saved config 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 ;; esac