From b2d990a8c3714b8992fcf60e0c6eb6b64bf3de12 Mon Sep 17 00:00:00 2001 From: Here ForAwhile Date: Mon, 16 Feb 2026 17:08:20 +0000 Subject: [PATCH] Upload New File --- terminalphone.sh | 1384 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1384 insertions(+) create mode 100644 terminalphone.sh diff --git a/terminalphone.sh b/terminalphone.sh new file mode 100644 index 0000000..2806e74 --- /dev/null +++ b/terminalphone.sh @@ -0,0 +1,1384 @@ +#!/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.0" +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_$$" + +# 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 + +# 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' +BG_BLUE='\033[44m' +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="" +LISTENER_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 +PTT_KEY="$PTT_KEY" +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() { + openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -pass "pass:${SHARED_SECRET}" -nosalt 2>/dev/null +} + +# Decrypt stdin to stdout +decrypt_stream() { + openssl enc -d -aes-256-cbc -pbkdf2 -iter 10000 -pass "pass:${SHARED_SECRET}" -nosalt 2>/dev/null +} + +# Encrypt a file +encrypt_file() { + local infile="$1" outfile="$2" + openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -pass "pass:${SHARED_SECRET}" \ + -in "$infile" -out "$outfile" 2>/dev/null +} + +# Decrypt a file +decrypt_file() { + local infile="$1" outfile="$2" + openssl enc -d -aes-256-cbc -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 +} + +# Record a chunk of audio, encode to opus, return the file path +record_chunk() { + local _id=$(uid) + local raw_file="$AUDIO_DIR/rec_${_id}.raw" + local opus_file="$AUDIO_DIR/rec_${_id}.opus" + + # Record raw audio + audio_record "$raw_file" "$CHUNK_DURATION" + + # Encode to opus + 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 + fi + + rm -f "$raw_file" + echo "$opus_file" +} + +# 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 +} + +#============================================================================= +# PROTOCOL — FRAMED MESSAGES +#============================================================================= +# Message format: [1 byte type][4 bytes length (big-endian)][payload] +# Types: 0x01 = voice data, 0x02 = PTT start, 0x03 = PTT stop, 0x04 = ping, 0x05 = text + +PROTO_VOICE=1 +PROTO_PTT_START=2 +PROTO_PTT_STOP=3 +PROTO_PING=4 +PROTO_TEXT=5 + +# Send a framed message over the connection fd +send_message() { + local msg_type="$1" + local payload_file="$2" # file containing payload, or empty + local fd="$3" + + local payload_len=0 + if [ -n "$payload_file" ] && [ -f "$payload_file" ]; then + payload_len=$(stat -c%s "$payload_file" 2>/dev/null || echo 0) + fi + + # Write header: type (1 byte) + length (4 bytes big-endian) + printf "\\x$(printf '%02x' "$msg_type")" >&"$fd" + printf "\\x$(printf '%02x' $(( (payload_len >> 24) & 0xFF )))" >&"$fd" + printf "\\x$(printf '%02x' $(( (payload_len >> 16) & 0xFF )))" >&"$fd" + printf "\\x$(printf '%02x' $(( (payload_len >> 8) & 0xFF )))" >&"$fd" + printf "\\x$(printf '%02x' $(( payload_len & 0xFF )))" >&"$fd" + + # Write payload + if [ "$payload_len" -gt 0 ]; then + cat "$payload_file" >&"$fd" + fi +} + +#============================================================================= +# CONNECTION HANDLER +#============================================================================= + +# Background process: handle receiving data from remote +receive_loop() { + local conn_fd="$1" + mkdir -p "$AUDIO_DIR" + + while [ -f "$CONNECTED_FLAG" ]; do + # Read message header (5 bytes: 1 type + 4 length) + local header + header=$(dd bs=1 count=5 <&"$conn_fd" 2>/dev/null | xxd -p) + + if [ -z "$header" ] || [ ${#header} -lt 10 ]; then + sleep 0.1 + continue + fi + + local msg_type=$((16#${header:0:2})) + local payload_len=$((16#${header:2:8})) + + case $msg_type in + $PROTO_VOICE) + if [ "$payload_len" -gt 0 ]; then + local _rid=$(uid) + local enc_file="$AUDIO_DIR/recv_enc_${_rid}.opus" + local dec_file="$AUDIO_DIR/recv_${_rid}.opus" + dd bs=1 count="$payload_len" <&"$conn_fd" > "$enc_file" 2>/dev/null + if decrypt_file "$enc_file" "$dec_file"; then + play_chunk "$dec_file" + fi + rm -f "$enc_file" "$dec_file" + fi + ;; + $PROTO_PTT_START) + echo -e "\r${BG_GREEN}${WHITE} ▶ REMOTE SPEAKING ${NC} " >&2 + ;; + $PROTO_PTT_STOP) + echo -e "\r${DIM} ■ Remote idle ${NC} " >&2 + ;; + $PROTO_PING) + # Respond with ping + ;; + *) + ;; + esac + done +} + +# Background process: handle sending based on PTT state +transmit_loop() { + local conn_fd="$1" + local was_pressed=0 + mkdir -p "$AUDIO_DIR" + + while [ -f "$CONNECTED_FLAG" ]; do + if [ -f "$PTT_FLAG" ]; then + if [ $was_pressed -eq 0 ]; then + # PTT just pressed — notify remote + was_pressed=1 + local empty_file="$AUDIO_DIR/empty_$(uid)" + touch "$empty_file" + send_message $PROTO_PTT_START "$empty_file" "$conn_fd" 2>/dev/null || true + rm -f "$empty_file" + fi + + # Record a chunk, encrypt, and send + local opus_file + opus_file=$(record_chunk) + if [ -f "$opus_file" ]; then + local enc_file="$AUDIO_DIR/send_enc_$(uid).opus" + if encrypt_file "$opus_file" "$enc_file"; then + send_message $PROTO_VOICE "$enc_file" "$conn_fd" 2>/dev/null || true + fi + rm -f "$opus_file" "$enc_file" + fi + else + if [ $was_pressed -eq 1 ]; then + # PTT just released — notify remote + was_pressed=0 + local empty_file="$AUDIO_DIR/empty_$(uid)" + touch "$empty_file" + send_message $PROTO_PTT_STOP "$empty_file" "$conn_fd" 2>/dev/null || true + rm -f "$empty_file" + fi + sleep 0.1 + fi + done +} + +# 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" "" + + # Cleanup after call ends + kill "$socat_pid" 2>/dev/null || true + rm -f "$CONNECTED_FLAG" "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag" +} + +# 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 + + rm -f "$CONNECTED_FLAG" "$RECV_PIPE" "$SEND_PIPE" +} + +#============================================================================= +# IN-CALL SESSION — PTT VOICE LOOP +#============================================================================= + +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" + + # 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 for caller ID + local my_onion + my_onion=$(get_onion) + if [ -n "$my_onion" ]; then + echo "ID:${my_onion}" >&4 2>/dev/null || true + fi + + # Remote address (populated by receive loop) + local remote_id_file="$DATA_DIR/run/remote_id_$$" + rm -f "$remote_id_file" + + # If we don't know the remote address yet (listener), wait briefly for handshake + local remote_display="$known_remote" + if [ -z "$remote_display" ]; then + # Read the first line — should be the ID handshake + 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" + fi + fi + fi + + # Show header with remote address + if [ -n "$remote_display" ]; then + echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC} ${CYAN}${remote_display}${NC}\n" + else + echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC}\n" + fi + echo -e " ${BOLD}Controls:${NC}" + echo -e " ${GREEN}[SPACE]${NC} -- Push-to-Talk" + echo -e " ${CYAN}[T]${NC} -- Send text message" + echo -e " ${RED}[Q]${NC} -- Hang up" + echo -e "" + + # 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" + ;; + 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 [Q]=Hang up${NC} " >&2 + else + echo -ne "\r ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [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 [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 [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 [Q]=Hang up${NC} " >&2 + else + echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [Q]=Hang up${NC} " >&2 + fi + fi + done + + # Restore terminal + stty "$ORIGINAL_STTY" 2>/dev/null || stty sane + ORIGINAL_STTY="" + + # Close pipe fds FIRST to unblock any blocking reads + rm -f "$PTT_FLAG" "$CONNECTED_FLAG" + exec 3<&- 2>/dev/null || true + exec 4>&- 2>/dev/null || true + + # Now kill background processes (they'll exit since fds are closed) + kill "$recv_pid" 2>/dev/null || true + if [ -n "$REC_PID" ]; then + kill "$REC_PID" 2>/dev/null || true + fi + # Brief wait, don't hang forever + sleep 0.5 + wait "$recv_pid" 2>/dev/null || true + + CALL_ACTIVE=0 + rm -f "$remote_id_file" + 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}Opus bitrate: ${OPUS_BITRATE}kbps${NC}" + echo -e " ${DIM}PTT key: [SPACEBAR]${NC}" + echo "" +} + +#============================================================================= +# 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}" + echo -e " ${DIM}v${VERSION} | Push-to-Talk | End-to-End AES-256${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}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 + ;; + 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