1384 lines
44 KiB
Bash
1384 lines
44 KiB
Bash
#!/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:<onion>, PTT_START, PTT_STOP, PING,
|
|
# or "AUDIO:<base64_encoded_encrypted_opus>"
|
|
(
|
|
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 <onion-address>"
|
|
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
|