2456 lines
87 KiB
Bash
2456 lines
87 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.1.1"
|
||
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_$$"
|
||
AUTO_LISTEN_FLAG="$DATA_DIR/run/autolisten_$$"
|
||
AUTO_LISTEN_PID=""
|
||
|
||
|
||
# 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
|
||
SNOWFLAKE_ENABLED=0 # Snowflake bridge (off by default)
|
||
AUTO_LISTEN=0 # Auto-listen after Tor starts (off by default)
|
||
VOICE_EFFECT="none" # Voice effect (none, deep, high, robot, echo, whisper, custom)
|
||
VOL_PTT=0 # Volume-down double-tap PTT (Termux only, experimental)
|
||
|
||
# Custom voice effect parameters (used when VOICE_EFFECT=custom)
|
||
VOICE_PITCH=0 # Pitch shift in cents (-600 to +600, 0=off)
|
||
VOICE_OVERDRIVE=0 # Overdrive gain (0=off, 5-20)
|
||
VOICE_FLANGER=0 # Flanger (0=off, 1=on)
|
||
VOICE_ECHO_DELAY=0 # Echo delay in ms (0=off, 20-200)
|
||
VOICE_ECHO_DECAY=5 # Echo decay (1-9 → 0.1-0.9)
|
||
VOICE_HIGHPASS=0 # Highpass filter freq in Hz (0=off, 300-2000)
|
||
VOICE_TREMOLO=0 # Tremolo speed in Hz (0=off, 5-40)
|
||
|
||
# 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() {
|
||
head -c6 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
||
}
|
||
|
||
load_config() {
|
||
if [ -f "$CONFIG_FILE" ]; then
|
||
source "$CONFIG_FILE"
|
||
fi
|
||
if [ -f "$SECRET_FILE" ]; then
|
||
# Check if the file is OpenSSL-encrypted (starts with "Salted__")
|
||
local magic
|
||
magic=$(head -c 8 "$SECRET_FILE" 2>/dev/null | cat -v)
|
||
if [[ "$magic" == "Salted__"* ]]; then
|
||
# Encrypted secret — prompt for passphrase
|
||
echo -ne " ${BOLD}Enter passphrase to unlock shared secret: ${NC}"
|
||
read -rs _unlock_pass
|
||
echo ""
|
||
if [ -n "$_unlock_pass" ]; then
|
||
SHARED_SECRET=$(openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 \
|
||
-pass "fd:3" -in "$SECRET_FILE" 3<<< "${_unlock_pass}" 2>/dev/null) || true
|
||
if [ -z "$SHARED_SECRET" ]; then
|
||
log_warn "Failed to unlock secret (wrong passphrase?)"
|
||
log_info "You can re-enter the secret with option 4"
|
||
else
|
||
log_ok "Shared secret unlocked"
|
||
fi
|
||
else
|
||
log_warn "No passphrase entered — secret not loaded"
|
||
SHARED_SECRET=""
|
||
fi
|
||
else
|
||
# Plaintext secret (legacy) — load directly
|
||
SHARED_SECRET=$(cat "$SECRET_FILE")
|
||
if [ -n "$SHARED_SECRET" ]; then
|
||
log_info "Plaintext secret detected"
|
||
echo -ne " ${BOLD}Protect it with a passphrase? [Y/n]: ${NC}"
|
||
read -r _migrate
|
||
if [ "$_migrate" != "n" ] && [ "$_migrate" != "N" ]; then
|
||
echo -ne " ${BOLD}Choose a passphrase: ${NC}"
|
||
read -rs _new_pass
|
||
echo ""
|
||
if [ -n "$_new_pass" ]; then
|
||
echo -ne " ${BOLD}Confirm passphrase: ${NC}"
|
||
read -rs _confirm_pass
|
||
echo ""
|
||
if [ "$_new_pass" = "$_confirm_pass" ]; then
|
||
echo -n "$SHARED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \
|
||
-pass "fd:3" -out "$SECRET_FILE" 3<<< "${_new_pass}" 2>/dev/null
|
||
chmod 600 "$SECRET_FILE"
|
||
log_ok "Secret encrypted with passphrase"
|
||
else
|
||
log_warn "Passphrases don't match — keeping plaintext"
|
||
fi
|
||
else
|
||
log_warn "Empty passphrase — keeping plaintext"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
SHARED_SECRET=""
|
||
fi
|
||
}
|
||
|
||
save_config() {
|
||
mkdir -p "$DATA_DIR"
|
||
cat > "$CONFIG_FILE" << EOF
|
||
LISTEN_PORT=$LISTEN_PORT
|
||
TOR_SOCKS_PORT=$TOR_SOCKS_PORT
|
||
OPUS_BITRATE=$OPUS_BITRATE
|
||
OPUS_FRAMESIZE=$OPUS_FRAMESIZE
|
||
PTT_KEY="$PTT_KEY"
|
||
CIPHER="$CIPHER"
|
||
SNOWFLAKE_ENABLED=$SNOWFLAKE_ENABLED
|
||
AUTO_LISTEN=$AUTO_LISTEN
|
||
VOICE_EFFECT="$VOICE_EFFECT"
|
||
VOICE_PITCH=$VOICE_PITCH
|
||
VOICE_OVERDRIVE=$VOICE_OVERDRIVE
|
||
VOICE_FLANGER=$VOICE_FLANGER
|
||
VOICE_ECHO_DELAY=$VOICE_ECHO_DELAY
|
||
VOICE_ECHO_DECAY=$VOICE_ECHO_DECAY
|
||
VOICE_HIGHPASS=$VOICE_HIGHPASS
|
||
VOICE_TREMOLO=$VOICE_TREMOLO
|
||
VOL_PTT=$VOL_PTT
|
||
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 -ne "\n${BOLD}Install missing dependencies? [Y/n]: ${NC}"
|
||
read -r _install_confirm
|
||
if [ "$_install_confirm" = "n" ] || [ "$_install_confirm" = "N" ]; then
|
||
echo -e "\n ${YELLOW}Installation skipped.${NC}"
|
||
return 1
|
||
fi
|
||
echo ""
|
||
|
||
# Use sudo only if available and not on Termux
|
||
local SUDO="sudo"
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
SUDO=""
|
||
log_info "Termux detected — installing without sudo"
|
||
elif ! check_dep sudo; then
|
||
SUDO=""
|
||
fi
|
||
|
||
# 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
|
||
|
||
# Append snowflake bridge config if enabled
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ] && check_dep snowflake-client; then
|
||
local sf_bin
|
||
sf_bin=$(command -v snowflake-client)
|
||
cat >> "$TOR_CONF" << EOF
|
||
|
||
UseBridges 1
|
||
ClientTransportPlugin snowflake exec $sf_bin
|
||
Bridge snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net/ ice=stun:stun.l.google.com:19302,stun:stun.voip.blackberry.com:3478
|
||
EOF
|
||
log_info "Snowflake bridge enabled in torrc"
|
||
elif [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
log_warn "Snowflake enabled but snowflake-client not found — skipping bridge"
|
||
fi
|
||
|
||
chmod 600 "$TOR_CONF"
|
||
}
|
||
|
||
install_snowflake() {
|
||
if check_dep snowflake-client; then
|
||
log_ok "snowflake-client already installed"
|
||
return 0
|
||
fi
|
||
|
||
log_info "Installing snowflake-client..."
|
||
|
||
local SUDO="sudo"
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
SUDO=""
|
||
elif ! check_dep sudo; then
|
||
SUDO=""
|
||
fi
|
||
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
pkg install -y snowflake-client
|
||
elif check_dep apt-get; then
|
||
$SUDO apt-get update -qq
|
||
$SUDO apt-get install -y snowflake-client
|
||
elif check_dep dnf; then
|
||
$SUDO dnf install -y snowflake-client
|
||
elif check_dep pacman; then
|
||
$SUDO pacman -S --noconfirm snowflake-pt-client
|
||
else
|
||
log_err "No supported package manager found. Install snowflake-client manually."
|
||
return 1
|
||
fi
|
||
|
||
if check_dep snowflake-client; then
|
||
log_ok "snowflake-client installed successfully"
|
||
else
|
||
log_err "snowflake-client installation failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
|
||
|
||
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..."
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
echo -e " ${DIM}Snowflake bridge enabled — this may take longer than usual, please be patient.${NC}"
|
||
fi
|
||
tor -f "$TOR_CONF" &>/dev/null &
|
||
TOR_PID=$!
|
||
save_pid "tor" "$TOR_PID"
|
||
|
||
# Monitor bootstrap progress from the log
|
||
local waited=0
|
||
local timeout=120
|
||
local last_pct=""
|
||
|
||
while [ $waited -lt $timeout ]; do
|
||
# Check if Tor is still running
|
||
if ! kill -0 "$TOR_PID" 2>/dev/null; then
|
||
echo ""
|
||
log_err "Tor process died! Check $tor_log"
|
||
[ -f "$tor_log" ] && tail -5 "$tor_log" 2>/dev/null
|
||
return 1
|
||
fi
|
||
|
||
# Parse latest bootstrap line from log
|
||
local bootstrap_line=""
|
||
bootstrap_line=$(grep -o "Bootstrapped [0-9]*%.*" "$tor_log" 2>/dev/null | tail -1 || true)
|
||
|
||
if [ -n "$bootstrap_line" ]; then
|
||
local pct=""
|
||
pct=$(echo "$bootstrap_line" | grep -o '[0-9]*%' || true)
|
||
if [ -n "$pct" ] && [ "$pct" != "$last_pct" ]; then
|
||
echo -ne "\r ${DIM}${bootstrap_line}${NC} "
|
||
last_pct="$pct"
|
||
fi
|
||
|
||
# Check for 100%
|
||
if [[ "$bootstrap_line" == *"100%"* ]]; then
|
||
echo ""
|
||
break
|
||
fi
|
||
else
|
||
# No bootstrap line yet, show waiting indicator
|
||
echo -ne "\r ${DIM}Waiting for Tor to start...${NC} "
|
||
fi
|
||
|
||
sleep 1
|
||
waited=$((waited + 1))
|
||
done
|
||
|
||
if [ $waited -ge $timeout ]; then
|
||
echo ""
|
||
log_err "Timed out waiting for Tor to bootstrap ($timeout seconds)"
|
||
return 1
|
||
fi
|
||
|
||
# Wait for onion address file (should appear quickly after 100%)
|
||
local addr_wait=0
|
||
while [ ! -f "$ONION_FILE" ] && [ $addr_wait -lt 15 ]; do
|
||
sleep 1
|
||
addr_wait=$((addr_wait + 1))
|
||
done
|
||
|
||
if [ -f "$ONION_FILE" ]; then
|
||
local onion
|
||
onion=$(cat "$ONION_FILE")
|
||
log_ok "Tor hidden service active"
|
||
echo -e " ${BOLD}${GREEN}Your address: ${WHITE}${onion}${NC}"
|
||
return 0
|
||
else
|
||
log_err "Tor bootstrapped but hidden service address not found"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
stop_tor() {
|
||
stop_auto_listener
|
||
if [ -n "$TOR_PID" ] && kill -0 "$TOR_PID" 2>/dev/null; then
|
||
kill "$TOR_PID" 2>/dev/null || true
|
||
wait "$TOR_PID" 2>/dev/null || true
|
||
TOR_PID=""
|
||
log_ok "Tor stopped"
|
||
else
|
||
log_info "Tor is not running"
|
||
fi
|
||
}
|
||
|
||
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 -ne "\n ${BOLD}Protect with a passphrase? [Y/n]: ${NC}"
|
||
read -r _protect
|
||
if [ "$_protect" != "n" ] && [ "$_protect" != "N" ]; then
|
||
echo -ne " ${BOLD}Choose a passphrase: ${NC}"
|
||
read -rs _pass
|
||
echo ""
|
||
if [ -n "$_pass" ]; then
|
||
echo -ne " ${BOLD}Confirm passphrase: ${NC}"
|
||
read -rs _pass2
|
||
echo ""
|
||
if [ "$_pass" = "$_pass2" ]; then
|
||
echo -n "$SHARED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 \
|
||
-pass "fd:3" -out "$SECRET_FILE" 3<<< "${_pass}" 2>/dev/null
|
||
chmod 600 "$SECRET_FILE"
|
||
log_ok "Shared secret saved (encrypted with passphrase)"
|
||
return
|
||
else
|
||
log_warn "Passphrases don't match"
|
||
fi
|
||
else
|
||
log_warn "Empty passphrase"
|
||
fi
|
||
log_info "Falling back to plaintext storage"
|
||
fi
|
||
|
||
# Plaintext fallback
|
||
echo -n "$SHARED_SECRET" > "$SECRET_FILE"
|
||
chmod 600 "$SECRET_FILE"
|
||
log_ok "Shared secret saved"
|
||
}
|
||
|
||
|
||
# Encrypt a file
|
||
encrypt_file() {
|
||
local infile="$1" outfile="$2"
|
||
local c="$CIPHER"
|
||
[ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE")
|
||
openssl enc -"${c}" -pbkdf2 -iter 10000 -pass "fd:3" \
|
||
-in "$infile" -out "$outfile" 3<<< "${SHARED_SECRET}" 2>/dev/null
|
||
}
|
||
|
||
# Decrypt a file
|
||
decrypt_file() {
|
||
local infile="$1" outfile="$2"
|
||
local c="$CIPHER"
|
||
[ -f "$CIPHER_RUNTIME_FILE" ] && c=$(cat "$CIPHER_RUNTIME_FILE")
|
||
openssl enc -d -"${c}" -pbkdf2 -iter 10000 -pass "fd:3" \
|
||
-in "$infile" -out "$outfile" 3<<< "${SHARED_SECRET}" 2>/dev/null
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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).tmp"
|
||
rm -f "$tmp_rec"
|
||
termux-microphone-record -l "$((duration + 1))" -f "$tmp_rec" &>/dev/null
|
||
sleep "$duration"
|
||
termux-microphone-record -q &>/dev/null || true
|
||
sleep 0.5
|
||
if [ -s "$tmp_rec" ]; then
|
||
ffmpeg -y -i "$tmp_rec" -f s16le -ar "$SAMPLE_RATE" -ac 1 \
|
||
"$outfile" &>/dev/null || log_warn "ffmpeg conversion failed"
|
||
fi
|
||
rm -f "$tmp_rec"
|
||
else
|
||
arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -d "$duration" \
|
||
-q "$outfile" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# Start continuous recording in background (returns immediately)
|
||
# Sets REC_PID and REC_FILE globals
|
||
start_recording() {
|
||
local _id=$(uid)
|
||
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
REC_FILE="$AUDIO_DIR/msg_${_id}.tmp"
|
||
rm -f "$REC_FILE"
|
||
termux-microphone-record -l 120 -f "$REC_FILE" &>/dev/null &
|
||
REC_PID=$!
|
||
else
|
||
REC_FILE="$AUDIO_DIR/msg_${_id}.tmp"
|
||
arecord -f S16_LE -r "$SAMPLE_RATE" -c 1 -t raw -q "$REC_FILE" 2>/dev/null &
|
||
REC_PID=$!
|
||
fi
|
||
}
|
||
|
||
# Apply voice effect to raw PCM using sox
|
||
apply_voice_effect() {
|
||
local infile="$1"
|
||
local outfile="$2"
|
||
local fmt="-t raw -r $SAMPLE_RATE -e signed -b 16 -c 1"
|
||
case "$VOICE_EFFECT" in
|
||
deep)
|
||
sox $fmt "$infile" $fmt "$outfile" pitch -400 2>/dev/null
|
||
;;
|
||
high)
|
||
sox $fmt "$infile" $fmt "$outfile" pitch 500 2>/dev/null
|
||
;;
|
||
robot)
|
||
sox $fmt "$infile" $fmt "$outfile" overdrive 10 flanger 2>/dev/null
|
||
;;
|
||
echo)
|
||
sox $fmt "$infile" $fmt "$outfile" echo 0.8 0.88 60 0.4 2>/dev/null
|
||
;;
|
||
whisper)
|
||
sox $fmt "$infile" $fmt "$outfile" highpass 1000 tremolo 20 2>/dev/null
|
||
;;
|
||
custom)
|
||
# Build sox effects chain from individual parameters
|
||
local effects=""
|
||
[ "$VOICE_PITCH" -ne 0 ] 2>/dev/null && effects="$effects pitch $VOICE_PITCH"
|
||
[ "$VOICE_OVERDRIVE" -gt 0 ] 2>/dev/null && effects="$effects overdrive $VOICE_OVERDRIVE"
|
||
[ "$VOICE_FLANGER" -eq 1 ] 2>/dev/null && effects="$effects flanger"
|
||
[ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null && effects="$effects echo 0.8 0.88 $VOICE_ECHO_DELAY 0.${VOICE_ECHO_DECAY}"
|
||
[ "$VOICE_HIGHPASS" -gt 0 ] 2>/dev/null && effects="$effects highpass $VOICE_HIGHPASS"
|
||
[ "$VOICE_TREMOLO" -gt 0 ] 2>/dev/null && effects="$effects tremolo $VOICE_TREMOLO"
|
||
if [ -n "$effects" ]; then
|
||
sox $fmt "$infile" $fmt "$outfile" $effects 2>/dev/null
|
||
else
|
||
return 1 # no effects configured
|
||
fi
|
||
;;
|
||
*)
|
||
return 1 # no effect
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Stop recording and send the message
|
||
# Encodes, encrypts, base64-encodes, and writes to fd 4
|
||
stop_and_send() {
|
||
local _id=$(uid)
|
||
local raw_file="$AUDIO_DIR/tx_${_id}.tmp"
|
||
local opus_file="$AUDIO_DIR/tx_o_${_id}.tmp"
|
||
local enc_file="$AUDIO_DIR/tx_e_${_id}.tmp"
|
||
|
||
# Stop the recording
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
termux-microphone-record -q &>/dev/null || true
|
||
kill "$REC_PID" 2>/dev/null || true
|
||
wait "$REC_PID" 2>/dev/null || true
|
||
sleep 0.3 # let file flush
|
||
# Convert m4a → raw PCM
|
||
if [ -s "$REC_FILE" ]; then
|
||
ffmpeg -y -i "$REC_FILE" -f s16le -ar "$SAMPLE_RATE" -ac 1 \
|
||
"$raw_file" &>/dev/null || true
|
||
fi
|
||
rm -f "$REC_FILE"
|
||
else
|
||
kill "$REC_PID" 2>/dev/null || true
|
||
wait "$REC_PID" 2>/dev/null || true
|
||
raw_file="$REC_FILE" # already in raw format
|
||
fi
|
||
|
||
REC_PID=""
|
||
REC_FILE=""
|
||
|
||
# Apply voice effect if set
|
||
if [ -s "$raw_file" ] && [ "$VOICE_EFFECT" != "none" ]; then
|
||
local fx_file="$AUDIO_DIR/tx_fx_${_id}.tmp"
|
||
if apply_voice_effect "$raw_file" "$fx_file"; then
|
||
mv "$fx_file" "$raw_file"
|
||
else
|
||
rm -f "$fx_file" 2>/dev/null
|
||
fi
|
||
fi
|
||
|
||
# Encode → encrypt → send
|
||
if [ -s "$raw_file" ]; then
|
||
opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \
|
||
--bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \
|
||
--speech --quiet \
|
||
"$raw_file" "$opus_file" 2>/dev/null
|
||
|
||
if [ -s "$opus_file" ]; then
|
||
encrypt_file "$opus_file" "$enc_file" 2>/dev/null
|
||
if [ -s "$enc_file" ]; then
|
||
local enc_size
|
||
enc_size=$(stat -c%s "$enc_file" 2>/dev/null || echo 0)
|
||
local size_kb=$(( enc_size * 10 / 1024 ))
|
||
local size_whole=$(( size_kb / 10 ))
|
||
local size_frac=$(( size_kb % 10 ))
|
||
|
||
local b64
|
||
b64=$(base64 -w 0 "$enc_file" 2>/dev/null)
|
||
echo "AUDIO:${b64}" >&4 2>/dev/null || true
|
||
LAST_SENT_INFO="${size_whole}.${size_frac}KB"
|
||
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 sox play (avoids Android MediaPlayer indexing)
|
||
play -q -t raw -r "$rate" -e signed -b 16 -c 1 "$infile" 2>/dev/null || true
|
||
else
|
||
# Linux: use ALSA aplay
|
||
aplay -f S16_LE -r "$rate" -c 1 -q "$infile" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
|
||
|
||
# Play an opus file
|
||
play_chunk() {
|
||
local opus_file="$1"
|
||
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
# Termux: pipe decode directly to sox play (avoids temp file + Android media framework)
|
||
opusdec --quiet --rate 48000 "$opus_file" - 2>/dev/null | \
|
||
play -q -t raw -r 48000 -e signed -b 16 -c 1 - 2>/dev/null || true
|
||
else
|
||
# Linux: pipe decode directly to aplay
|
||
opusdec --quiet --rate 48000 "$opus_file" - 2>/dev/null | \
|
||
aplay -f S16_LE -r 48000 -c 1 -q 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
|
||
|
||
#=============================================================================
|
||
# CALL CLEANUP — RESET EVERYTHING TO FRESH STATE
|
||
#=============================================================================
|
||
|
||
cleanup_call() {
|
||
# Restore terminal to sane state
|
||
if [ -n "$ORIGINAL_STTY" ]; then
|
||
stty "$ORIGINAL_STTY" 2>/dev/null || true
|
||
fi
|
||
stty sane 2>/dev/null || true
|
||
ORIGINAL_STTY=""
|
||
|
||
# Close pipe file descriptors to unblock any blocking reads
|
||
# NOTE: must use { } group so 2>/dev/null doesn't permanently redirect stderr
|
||
{ exec 3<&-; } 2>/dev/null || true
|
||
{ exec 4>&-; } 2>/dev/null || true
|
||
|
||
# Kill all call-related processes by PID files
|
||
for pidfile in "$PID_DIR"/socat.pid "$PID_DIR"/socat_call.pid "$PID_DIR"/recv_loop.pid; do
|
||
if [ -f "$pidfile" ]; then
|
||
local pid
|
||
pid=$(cat "$pidfile" 2>/dev/null)
|
||
if [ -n "$pid" ]; then
|
||
kill "$pid" 2>/dev/null || true
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
fi
|
||
rm -f "$pidfile"
|
||
fi
|
||
done
|
||
|
||
# Kill recording process if active
|
||
if [ -n "$REC_PID" ]; then
|
||
kill "$REC_PID" 2>/dev/null || true
|
||
kill -9 "$REC_PID" 2>/dev/null || true
|
||
REC_PID=""
|
||
fi
|
||
|
||
# Wait briefly for processes to die
|
||
sleep 0.2
|
||
|
||
# Kill volume monitor if active
|
||
if [ -n "$VOL_MON_PID" ]; then
|
||
kill "$VOL_MON_PID" 2>/dev/null || true
|
||
kill -9 "$VOL_MON_PID" 2>/dev/null || true
|
||
VOL_MON_PID=""
|
||
fi
|
||
|
||
# Remove all runtime files for this PID
|
||
rm -f "$PTT_FLAG" "$CONNECTED_FLAG"
|
||
rm -f "$RECV_PIPE" "$SEND_PIPE"
|
||
rm -f "$CIPHER_RUNTIME_FILE"
|
||
rm -f "$DATA_DIR/run/remote_id_$$"
|
||
rm -f "$DATA_DIR/run/remote_cipher_$$"
|
||
rm -f "$DATA_DIR/run/incoming_$$"
|
||
rm -f "$DATA_DIR/run/vol_ptt_trigger_$$"
|
||
|
||
# Clean temp audio files
|
||
rm -f "$AUDIO_DIR"/*.tmp 2>/dev/null || true
|
||
|
||
# Reset state variables
|
||
CALL_ACTIVE=0
|
||
REC_PID=""
|
||
}
|
||
#=============================================================================
|
||
# AUTO-LISTEN (BACKGROUND LISTENER)
|
||
#=============================================================================
|
||
|
||
start_auto_listener() {
|
||
# Only start if auto-listen is enabled and Tor is running
|
||
if [ "$AUTO_LISTEN" -ne 1 ]; then return 0; fi
|
||
if [ -z "$SHARED_SECRET" ]; then return 0; fi
|
||
if [ -z "$TOR_PID" ] || ! kill -0 "$TOR_PID" 2>/dev/null; then return 0; fi
|
||
|
||
# Stop any existing listener first
|
||
stop_auto_listener
|
||
|
||
mkdir -p "$AUDIO_DIR" "$DATA_DIR/run"
|
||
rm -f "$RECV_PIPE" "$SEND_PIPE" "$AUTO_LISTEN_FLAG"
|
||
mkfifo "$RECV_PIPE" "$SEND_PIPE"
|
||
|
||
socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \
|
||
"SYSTEM:touch $AUTO_LISTEN_FLAG; cat $SEND_PIPE & cat > $RECV_PIPE" &
|
||
AUTO_LISTEN_PID=$!
|
||
save_pid "socat" "$AUTO_LISTEN_PID"
|
||
}
|
||
|
||
stop_auto_listener() {
|
||
if [ -n "$AUTO_LISTEN_PID" ]; then
|
||
kill "$AUTO_LISTEN_PID" 2>/dev/null || true
|
||
kill -9 "$AUTO_LISTEN_PID" 2>/dev/null || true
|
||
AUTO_LISTEN_PID=""
|
||
fi
|
||
rm -f "$AUTO_LISTEN_FLAG" "$RECV_PIPE" "$SEND_PIPE"
|
||
}
|
||
|
||
#=============================================================================
|
||
# VOLUME-DOWN DOUBLE-TAP PTT MONITOR (Termux only, experimental)
|
||
#=============================================================================
|
||
VOL_MON_PID=""
|
||
|
||
start_vol_monitor() {
|
||
local trigger_file="${1:-$DATA_DIR/run/vol_ptt_trigger_$$}"
|
||
[ "$VOL_PTT" -ne 1 ] && return
|
||
[ "$IS_TERMUX" -ne 1 ] && return
|
||
if ! check_dep jq; then
|
||
log_warn "jq not found — Volume PTT disabled"
|
||
return
|
||
fi
|
||
if ! check_dep termux-volume; then
|
||
log_warn "termux-volume not found — Volume PTT disabled"
|
||
return
|
||
fi
|
||
|
||
rm -f "$trigger_file"
|
||
|
||
(
|
||
local last_vol=""
|
||
local last_tap=0
|
||
local restore_vol=""
|
||
|
||
while [ -f "$CONNECTED_FLAG" ]; do
|
||
local cur_vol
|
||
cur_vol=$(termux-volume 2>/dev/null \
|
||
| jq -r '.[] | select(.stream=="music") | .volume' 2>/dev/null \
|
||
|| echo "")
|
||
|
||
# Remember the initial volume so we can restore after detection
|
||
if [ -n "$cur_vol" ] && [ -z "$restore_vol" ]; then
|
||
restore_vol="$cur_vol"
|
||
fi
|
||
|
||
if [ -n "$cur_vol" ] && [ -n "$last_vol" ]; then
|
||
local drop=$(( last_vol - cur_vol ))
|
||
|
||
if [ "$drop" -ge 2 ] 2>/dev/null; then
|
||
# Rapid double-tap: both presses landed in one poll cycle
|
||
touch "$trigger_file"
|
||
last_tap=0
|
||
# Restore volume for next use
|
||
if [ -n "$restore_vol" ]; then
|
||
termux-volume music "$restore_vol" 2>/dev/null || true
|
||
last_vol="$restore_vol"
|
||
sleep 0.5
|
||
continue
|
||
fi
|
||
elif [ "$drop" -ge 1 ] 2>/dev/null; then
|
||
# Single press — check if second press follows within 1s
|
||
local now
|
||
now=$(date +%s)
|
||
if [ "$last_tap" -gt 0 ] && [ $((now - last_tap)) -le 1 ]; then
|
||
touch "$trigger_file"
|
||
last_tap=0
|
||
if [ -n "$restore_vol" ]; then
|
||
termux-volume music "$restore_vol" 2>/dev/null || true
|
||
last_vol="$restore_vol"
|
||
sleep 0.5
|
||
continue
|
||
fi
|
||
else
|
||
last_tap=$now
|
||
fi
|
||
fi
|
||
fi
|
||
last_vol="$cur_vol"
|
||
sleep 0.4
|
||
done
|
||
) &
|
||
VOL_MON_PID=$!
|
||
}
|
||
|
||
stop_vol_monitor() {
|
||
if [ -n "$VOL_MON_PID" ]; then
|
||
kill "$VOL_MON_PID" 2>/dev/null || true
|
||
kill -9 "$VOL_MON_PID" 2>/dev/null || true
|
||
VOL_MON_PID=""
|
||
fi
|
||
rm -f "$DATA_DIR/run/vol_ptt_trigger_$$"
|
||
}
|
||
|
||
# Check if an incoming call arrived on the background listener
|
||
check_auto_listen() {
|
||
if [ -f "$AUTO_LISTEN_FLAG" ]; then
|
||
rm -f "$AUTO_LISTEN_FLAG"
|
||
touch "$CONNECTED_FLAG"
|
||
echo -e "\n ${GREEN}${BOLD}Incoming call detected!${NC}" >&2
|
||
sleep 0.5
|
||
in_call_session "$RECV_PIPE" "$SEND_PIPE" ""
|
||
cleanup_call
|
||
# Restart listener for next call
|
||
start_auto_listener
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# Start listening for incoming calls (manual, blocking)
|
||
listen_for_call() {
|
||
if [ -z "$SHARED_SECRET" ]; then
|
||
log_err "No shared secret set! Use option 4 first."
|
||
return 1
|
||
fi
|
||
|
||
start_tor || return 1
|
||
|
||
# Stop auto-listener if running (we'll do manual listen)
|
||
stop_auto_listener
|
||
|
||
local onion
|
||
onion=$(get_onion)
|
||
echo -e "\n${BOLD}${CYAN}═══ Listening for Calls ═══${NC}\n"
|
||
echo -e " ${GREEN}Your address:${NC} ${BOLD}${WHITE}$onion${NC}"
|
||
echo -e " ${GREEN}Listening on:${NC} port $LISTEN_PORT"
|
||
echo -e "\n ${DIM}Share your .onion address with the caller.${NC}"
|
||
echo -e " ${DIM}Press Ctrl+C to stop listening.${NC}\n"
|
||
|
||
mkdir -p "$AUDIO_DIR"
|
||
log_info "Waiting for incoming connection..."
|
||
|
||
rm -f "$RECV_PIPE" "$SEND_PIPE"
|
||
mkfifo "$RECV_PIPE" "$SEND_PIPE"
|
||
|
||
local incoming_flag="$DATA_DIR/run/incoming_$$"
|
||
rm -f "$incoming_flag"
|
||
|
||
socat "TCP-LISTEN:$LISTEN_PORT,reuseaddr" \
|
||
"SYSTEM:touch $incoming_flag; cat $SEND_PIPE & cat > $RECV_PIPE" &
|
||
local socat_pid=$!
|
||
save_pid "socat" "$socat_pid"
|
||
|
||
while [ ! -f "$incoming_flag" ]; do
|
||
if ! kill -0 "$socat_pid" 2>/dev/null; then
|
||
log_err "Listener stopped unexpectedly"
|
||
rm -f "$RECV_PIPE" "$SEND_PIPE" "$incoming_flag"
|
||
# Restart auto-listener if enabled
|
||
start_auto_listener
|
||
return 1
|
||
fi
|
||
sleep 0.5
|
||
done
|
||
|
||
touch "$CONNECTED_FLAG"
|
||
log_ok "Call connected!"
|
||
in_call_session "$RECV_PIPE" "$SEND_PIPE" ""
|
||
cleanup_call
|
||
|
||
# Restart auto-listener if enabled
|
||
start_auto_listener
|
||
}
|
||
|
||
# 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"
|
||
|
||
# Animated connecting indicator while socat establishes connection
|
||
(
|
||
local dots=""
|
||
while true; do
|
||
for dots in "." ".." "..." " "; do
|
||
echo -ne "\r ${CYAN}${BOLD}Connecting${dots}${NC} " >&2
|
||
sleep 0.3
|
||
done
|
||
done
|
||
) &
|
||
local spinner_pid=$!
|
||
|
||
# Give socat a moment to connect
|
||
sleep 2
|
||
|
||
if kill -0 "$socat_pid" 2>/dev/null; then
|
||
in_call_session "$RECV_PIPE" "$SEND_PIPE" "$remote_onion" "$spinner_pid"
|
||
else
|
||
# Kill spinner and show error
|
||
kill "$spinner_pid" 2>/dev/null || true
|
||
wait "$spinner_pid" 2>/dev/null || true
|
||
echo -ne "\r " >&2
|
||
echo "" >&2
|
||
log_err "Failed to connect. Check the address and ensure Tor is running."
|
||
fi
|
||
|
||
# Full cleanup after call ends
|
||
cleanup_call
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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
|
||
# Row counter for ANSI cursor positioning (clear sets cursor to row 1)
|
||
local _r=1
|
||
|
||
if [ -n "$_remote" ]; then
|
||
echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC} ${CYAN}${_remote}${NC}\n" >&2
|
||
else
|
||
echo -e "\n${BOLD}${BG_GREEN}${WHITE} CALL CONNECTED ${NC}\n" >&2
|
||
fi
|
||
_r=4 # \n(row1) + header(row2) + \n(row3) + echo-newline → cursor at row 4
|
||
|
||
# Cipher info
|
||
CIPHER_ROW=$_r
|
||
local cipher_upper="${CIPHER^^}"
|
||
if [ -n "$_rcipher" ]; then
|
||
local rcipher_upper="${_rcipher^^}"
|
||
if [ "$_rcipher" = "$CIPHER" ]; then
|
||
echo -e " ${GREEN}●${NC} Local cipher: ${WHITE}${cipher_upper}${NC}" >&2
|
||
echo -e " ${GREEN}●${NC} Remote cipher: ${WHITE}${rcipher_upper}${NC}" >&2
|
||
else
|
||
echo -e " ${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
|
||
_r=$((_r + 2))
|
||
|
||
# Snowflake bridge info
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
local tor_log="$TOR_DIR/tor.log"
|
||
echo "" >&2; _r=$((_r + 1))
|
||
echo -e " ${TOR_PURPLE}●${NC} ${BOLD}Snowflake bridge${NC}" >&2; _r=$((_r + 1))
|
||
if [ -f "$tor_log" ]; then
|
||
local bridge_line=""
|
||
bridge_line=$(grep "new bridge descriptor" "$tor_log" 2>/dev/null | tail -1 || true)
|
||
if [ -n "$bridge_line" ]; then
|
||
local bridge_name=""
|
||
bridge_name=$(echo "$bridge_line" | sed -n "s/.*new bridge descriptor '\([^']*\)'.*/\1/p" || true)
|
||
local bridge_fp=""
|
||
bridge_fp=$(echo "$bridge_line" | sed -n 's/.*(\(fresh\|stale\)): \(.*\)/\2/p' || true)
|
||
if [ -n "$bridge_name" ]; then
|
||
local fp_display="$bridge_fp"
|
||
if [ ${#fp_display} -gt 40 ]; then
|
||
fp_display="${fp_display:0:40}..."
|
||
fi
|
||
echo -e " ${DIM}descriptor:${NC} ${WHITE}${bridge_name}${NC} ${DIM}— ${fp_display}${NC}" >&2
|
||
_r=$((_r + 1))
|
||
fi
|
||
fi
|
||
local proxy_line=""
|
||
proxy_line=$(grep 'Managed proxy.*snowflake' "$tor_log" 2>/dev/null | tail -1 || true)
|
||
if [ -n "$proxy_line" ]; then
|
||
if echo "$proxy_line" | grep -q "connected"; then
|
||
echo -e " ${DIM}transport:${NC} ${GREEN}connected${NC}" >&2
|
||
else
|
||
echo -e " ${DIM}transport:${NC} ${YELLOW}connecting...${NC}" >&2
|
||
fi
|
||
_r=$((_r + 1))
|
||
fi
|
||
fi
|
||
fi
|
||
echo "" >&2; _r=$((_r + 1))
|
||
|
||
# Static placeholders — updated in-place via ANSI positioning
|
||
echo -e " ${DIM}Last sent: --${NC}" >&2
|
||
SENT_INFO_ROW=$_r
|
||
_r=$((_r + 1))
|
||
|
||
echo -e " ${DIM}Last recv: --${NC}" >&2
|
||
RECV_INFO_ROW=$_r
|
||
_r=$((_r + 1))
|
||
|
||
echo "" >&2; _r=$((_r + 1))
|
||
|
||
# Static status bar
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2
|
||
else
|
||
echo -ne " ${GREEN}${BOLD} Ready ${NC} ${DIM}[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up${NC} " >&2
|
||
fi
|
||
STATUS_ROW=$_r
|
||
|
||
echo "" >&2
|
||
echo "" >&2
|
||
}
|
||
|
||
in_call_session() {
|
||
local recv_pipe="$1"
|
||
local send_pipe="$2"
|
||
local known_remote="${3:-}"
|
||
local spinner_pid="${4:-}"
|
||
|
||
CALL_ACTIVE=1
|
||
rm -f "$PTT_FLAG"
|
||
mkdir -p "$AUDIO_DIR"
|
||
|
||
# Start volume-down double-tap monitor (Termux only)
|
||
VOL_MON_PID=""
|
||
local vol_trigger_file="$DATA_DIR/run/vol_ptt_trigger_$$"
|
||
rm -f "$vol_trigger_file"
|
||
start_vol_monitor "$vol_trigger_file"
|
||
|
||
# Write cipher to runtime file so subshells can track changes
|
||
echo "$CIPHER" > "$CIPHER_RUNTIME_FILE"
|
||
|
||
# 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
|
||
|
||
# Kill connecting spinner and draw call header
|
||
if [ -n "$spinner_pid" ]; then
|
||
kill "$spinner_pid" 2>/dev/null || true
|
||
wait "$spinner_pid" 2>/dev/null || true
|
||
fi
|
||
draw_call_header "$remote_display" "$remote_cipher"
|
||
|
||
# Start 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)
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
printf ' \033[42;1;37m REMOTE TALKING \033[0m ' >&2
|
||
printf '\033[u' >&2
|
||
;;
|
||
PTT_STOP)
|
||
# silent — Last recv row provides feedback
|
||
;;
|
||
PING)
|
||
# silent — no display update
|
||
;;
|
||
ID:*)
|
||
# Caller ID received (save but don't print — already in header)
|
||
local remote_addr="${line#ID:}"
|
||
echo "$remote_addr" > "$remote_id_file"
|
||
;;
|
||
CIPHER:*)
|
||
# Remote side sent/changed their cipher — save and update display
|
||
local rc="${line#CIPHER:}"
|
||
echo "$rc" > "$remote_cipher_file" 2>/dev/null || true
|
||
# Read current local cipher from runtime file
|
||
local _cur_cipher="$CIPHER"
|
||
[ -f "$CIPHER_RUNTIME_FILE" ] && _cur_cipher=$(cat "$CIPHER_RUNTIME_FILE")
|
||
local _cu="${_cur_cipher^^}"
|
||
local _ru="${rc^^}"
|
||
# Update cipher lines in-place using ANSI cursor positioning (rows 4-5)
|
||
local _dot_color
|
||
if [ "$rc" = "$_cur_cipher" ]; then
|
||
_dot_color="$GREEN"
|
||
else
|
||
_dot_color="$RED"
|
||
fi
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$CIPHER_ROW" >&2
|
||
printf ' %b●%b Local cipher: %b%s%b\r\n' "$_dot_color" "$NC" "$WHITE" "$_cu" "$NC" >&2
|
||
printf '\033[K' >&2
|
||
printf ' %b●%b Remote cipher: %b%s%b' "$_dot_color" "$NC" "$WHITE" "$_ru" "$NC" >&2
|
||
printf '\033[u' >&2
|
||
;;
|
||
MSG:*)
|
||
# Encrypted text message received
|
||
local msg_b64="${line#MSG:}"
|
||
local _mid=$(uid)
|
||
local msg_enc="$AUDIO_DIR/msg_enc_${_mid}.tmp"
|
||
local msg_dec="$AUDIO_DIR/msg_dec_${_mid}.tmp"
|
||
echo "$msg_b64" | base64 -d > "$msg_enc" 2>/dev/null || true
|
||
if [ -s "$msg_enc" ]; then
|
||
if decrypt_file "$msg_enc" "$msg_dec" 2>/dev/null; then
|
||
local msg_text
|
||
msg_text=$(cat "$msg_dec" 2>/dev/null)
|
||
echo -e "\n ${MAGENTA}${BOLD}[MSG]${NC} ${WHITE}${msg_text}${NC}" >&2
|
||
fi
|
||
fi
|
||
rm -f "$msg_enc" "$msg_dec" 2>/dev/null
|
||
;;
|
||
AUDIO:*)
|
||
# Extract base64 data, decode, decrypt, play
|
||
local b64_data="${line#AUDIO:}"
|
||
local _rid=$(uid)
|
||
local enc_file="$AUDIO_DIR/recv_enc_${_rid}.tmp"
|
||
local dec_file="$AUDIO_DIR/recv_dec_${_rid}.tmp"
|
||
|
||
echo "$b64_data" | base64 -d > "$enc_file" 2>/dev/null || true
|
||
if [ -s "$enc_file" ]; then
|
||
if decrypt_file "$enc_file" "$dec_file" 2>/dev/null; then
|
||
# Calculate recv size
|
||
local _enc_sz=0
|
||
_enc_sz=$(stat -c%s "$enc_file" 2>/dev/null || echo 0)
|
||
local _sz_kb=$(( _enc_sz * 10 / 1024 ))
|
||
local _sz_w=$(( _sz_kb / 10 ))
|
||
local _sz_f=$(( _sz_kb % 10 ))
|
||
local _recv_info="${_sz_w}.${_sz_f}KB"
|
||
# Update static "Last recv" row via ANSI positioning
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$RECV_INFO_ROW" >&2
|
||
printf ' \033[2mLast recv: \033[0m\033[1;37m%s\033[0m' "$_recv_info" >&2
|
||
printf '\033[u' >&2
|
||
|
||
play_chunk "$dec_file" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
rm -f "$enc_file" "$dec_file" 2>/dev/null
|
||
;;
|
||
HANGUP)
|
||
# Remote party hung up
|
||
echo -e "\r\n\r\n ${YELLOW}${BOLD}Remote party hung up.${NC}" >&2
|
||
rm -f "$CONNECTED_FLAG"
|
||
break
|
||
;;
|
||
esac
|
||
else
|
||
# Pipe closed or error — connection lost
|
||
echo -e "\r\n\r\n ${RED}${BOLD}Connection lost.${NC}" >&2
|
||
rm -f "$CONNECTED_FLAG"
|
||
break
|
||
fi
|
||
done
|
||
) &
|
||
local recv_pid=$!
|
||
save_pid "recv_loop" "$recv_pid"
|
||
|
||
# Main PTT input loop
|
||
ORIGINAL_STTY=$(stty -g)
|
||
stty raw -echo -icanon min 0 time 1
|
||
|
||
REC_PID=""
|
||
REC_FILE=""
|
||
LAST_SENT_INFO=""
|
||
local ptt_active=0
|
||
|
||
# Status bar is already drawn by draw_call_header
|
||
|
||
while [ -f "$CONNECTED_FLAG" ]; do
|
||
local key=""
|
||
# Check volume-down double-tap trigger
|
||
if [ -f "$vol_trigger_file" ]; then
|
||
rm -f "$vol_trigger_file"
|
||
key="$PTT_KEY" # simulate PTT key press
|
||
else
|
||
key=$(dd bs=1 count=1 2>/dev/null) || true
|
||
fi
|
||
|
||
if [ "$key" = "$PTT_KEY" ]; then
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
# TERMUX: Toggle mode
|
||
if [ $ptt_active -eq 0 ]; then
|
||
ptt_active=1
|
||
printf '\033[s' >&2; printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
printf ' \033[41;1;37m \u25cf RECORDING \033[0m \033[2m[SPACE]=Send\033[0m ' >&2
|
||
printf '\033[u' >&2
|
||
start_recording
|
||
else
|
||
ptt_active=0
|
||
stop_and_send
|
||
echo "PTT_STOP" >&4 2>/dev/null || true
|
||
# Update Last sent + status bar
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2
|
||
printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2
|
||
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
|
||
printf '\033[u' >&2
|
||
fi
|
||
else
|
||
# LINUX: Hold-to-talk
|
||
if [ $ptt_active -eq 0 ]; then
|
||
ptt_active=1
|
||
printf '\033[s' >&2; printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
printf ' \033[41;1;37;5m \u25cf RECORDING \033[0m ' >&2
|
||
printf '\033[u' >&2
|
||
stty time 5 # longer timeout to span keyboard repeat delay
|
||
start_recording
|
||
fi
|
||
fi
|
||
|
||
elif [ "$key" = "q" ] || [ "$key" = "Q" ]; then
|
||
# If recording, cancel it
|
||
if [ $ptt_active -eq 1 ] && [ -n "$REC_PID" ]; then
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
termux-microphone-record -q &>/dev/null || true
|
||
fi
|
||
kill "$REC_PID" 2>/dev/null || true
|
||
wait "$REC_PID" 2>/dev/null || true
|
||
rm -f "$REC_FILE" 2>/dev/null
|
||
REC_PID=""
|
||
REC_FILE=""
|
||
fi
|
||
echo -e "\r\n${YELLOW}Hanging up...${NC}" >&2
|
||
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
|
||
stty time 1 # restore fast timeout for key detection
|
||
ptt_active=0
|
||
stop_and_send
|
||
echo "PTT_STOP" >&4 2>/dev/null || true
|
||
# Update Last sent + status bar
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$SENT_INFO_ROW" >&2
|
||
printf ' \033[2mLast sent: \033[0m\033[1;37m%s\033[0m' "$LAST_SENT_INFO" >&2
|
||
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
printf ' \033[1;32m Sent! \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
|
||
printf '\033[u' >&2
|
||
fi
|
||
|
||
elif [ "$key" = "t" ] || [ "$key" = "T" ]; then
|
||
# Text chat mode
|
||
# Switch to cooked mode for text input
|
||
stty "$ORIGINAL_STTY" 2>/dev/null || stty sane
|
||
echo "" >&2
|
||
echo -ne " ${CYAN}${BOLD}MSG>${NC} " >&2
|
||
local chat_msg=""
|
||
read -r chat_msg
|
||
if [ -n "$chat_msg" ]; then
|
||
# Encrypt and send
|
||
local _cid=$(uid)
|
||
local chat_plain="$AUDIO_DIR/chat_${_cid}.tmp"
|
||
local chat_enc="$AUDIO_DIR/chat_enc_${_cid}.tmp"
|
||
echo -n "$chat_msg" > "$chat_plain"
|
||
encrypt_file "$chat_plain" "$chat_enc" 2>/dev/null
|
||
if [ -s "$chat_enc" ]; then
|
||
local chat_b64
|
||
chat_b64=$(base64 -w 0 "$chat_enc" 2>/dev/null)
|
||
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
|
||
# Restore status bar to Ready
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
|
||
else
|
||
printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
|
||
fi
|
||
printf '\033[u' >&2
|
||
|
||
elif [ "$key" = "s" ] || [ "$key" = "S" ]; then
|
||
# Mid-call settings
|
||
stty "$ORIGINAL_STTY" 2>/dev/null || stty sane
|
||
# Flush any leftover raw mode input
|
||
read -r -t 0.1 -n 10000 2>/dev/null || true
|
||
settings_menu
|
||
# Redraw call header and switch back to raw mode
|
||
local _rd="" _rc=""
|
||
[ -f "$remote_id_file" ] && _rd=$(cat "$remote_id_file" 2>/dev/null)
|
||
[ -z "$_rd" ] && _rd="$known_remote"
|
||
[ -f "$remote_cipher_file" ] && _rc=$(cat "$remote_cipher_file" 2>/dev/null)
|
||
draw_call_header "$_rd" "$_rc"
|
||
stty raw -echo -icanon min 0 time 1
|
||
# Restore status bar to Ready (header was redrawn, STATUS_ROW is fresh)
|
||
printf '\033[s' >&2
|
||
printf '\033[%d;1H\033[K' "$STATUS_ROW" >&2
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
|
||
else
|
||
printf ' \033[1;32m Ready \033[0m \033[2m[SPACE]=Hold to Talk [T]=Chat [S]=Settings [Q]=Hang up\033[0m ' >&2
|
||
fi
|
||
printf '\033[u' >&2
|
||
fi
|
||
done
|
||
|
||
echo -e "\n${BOLD}${RED} CALL ENDED ${NC}\n"
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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}.tmp"
|
||
audio_record "$raw_file" 3
|
||
echo -e "${GREEN}done${NC}"
|
||
|
||
if [ ! -s "$raw_file" ]; then
|
||
log_err "Recording failed — check your microphone"
|
||
return 1
|
||
fi
|
||
|
||
local raw_size
|
||
raw_size=$(stat -c%s "$raw_file")
|
||
echo -e " ${DIM}Recorded $raw_size bytes of raw audio${NC}"
|
||
|
||
# Step 2: Encode with Opus
|
||
echo -ne " ${YELLOW}● Encoding with Opus at ${OPUS_BITRATE}kbps...${NC} "
|
||
local opus_file="$AUDIO_DIR/test_${_tid}.tmp"
|
||
opusenc --raw --raw-rate "$SAMPLE_RATE" --raw-chan 1 \
|
||
--bitrate "$OPUS_BITRATE" --framesize "$OPUS_FRAMESIZE" \
|
||
--speech --quiet \
|
||
"$raw_file" "$opus_file" 2>/dev/null
|
||
echo -e "${GREEN}done${NC}"
|
||
|
||
if [ ! -s "$opus_file" ]; then
|
||
log_err "Opus encoding failed"
|
||
rm -f "$raw_file"
|
||
return 1
|
||
fi
|
||
|
||
local opus_size
|
||
opus_size=$(stat -c%s "$opus_file")
|
||
echo -e " ${DIM}Opus size: $opus_size bytes (compression ratio: $((raw_size / opus_size))x)${NC}"
|
||
|
||
# Step 3: Encrypt + Decrypt round-trip (if secret is set)
|
||
if [ -n "$SHARED_SECRET" ]; then
|
||
echo -ne " ${YELLOW}● Encrypting and decrypting...${NC} "
|
||
local enc_file="$AUDIO_DIR/test_enc_${_tid}.tmp"
|
||
local dec_file="$AUDIO_DIR/test_dec_${_tid}.tmp"
|
||
encrypt_file "$opus_file" "$enc_file"
|
||
decrypt_file "$enc_file" "$dec_file"
|
||
|
||
if cmp -s "$opus_file" "$dec_file"; then
|
||
echo -e "${GREEN}encryption round-trip OK${NC}"
|
||
else
|
||
echo -e "${RED}encryption round-trip FAILED${NC}"
|
||
fi
|
||
rm -f "$enc_file"
|
||
opus_file="$dec_file"
|
||
fi
|
||
|
||
# Step 4: Decode and play
|
||
echo -ne " ${YELLOW}● Playing back...${NC} "
|
||
play_chunk "$opus_file"
|
||
echo -e "${GREEN}done${NC}"
|
||
|
||
rm -f "$raw_file" "$opus_file" "$AUDIO_DIR/test_dec_${_tid}.tmp" 2>/dev/null
|
||
|
||
echo -e "\n ${GREEN}${BOLD}Audio test complete!${NC}"
|
||
echo -e " ${DIM}If you heard your voice, the pipeline is working.${NC}\n"
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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
|
||
|
||
# Snowflake
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
if check_dep snowflake-client; then
|
||
echo -e " ${GREEN}●${NC} Snowflake bridge enabled"
|
||
else
|
||
echo -e " ${YELLOW}●${NC} Snowflake enabled (client not installed)"
|
||
fi
|
||
else
|
||
echo -e " ${DIM}●${NC} Snowflake bridge disabled"
|
||
fi
|
||
|
||
# Config
|
||
echo -e "\n ${DIM}Listen port: $LISTEN_PORT${NC}"
|
||
echo -e " ${DIM}SOCKS port: $TOR_SOCKS_PORT${NC}"
|
||
echo -e " ${DIM}Cipher: $CIPHER${NC}"
|
||
echo -e " ${DIM}Opus bitrate: ${OPUS_BITRATE}kbps${NC}"
|
||
echo -e " ${DIM}Opus frame: ${OPUS_FRAMESIZE}ms${NC}"
|
||
echo -e " ${DIM}PTT key: [SPACEBAR]${NC}"
|
||
echo ""
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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}"
|
||
|
||
local sf_label="${RED}disabled${NC}"
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
sf_label="${GREEN}enabled${NC}"
|
||
fi
|
||
echo -e " ${DIM}Snowflake bridge: ${NC}${sf_label}"
|
||
|
||
local al_label="${RED}disabled${NC}"
|
||
if [ "$AUTO_LISTEN" -eq 1 ]; then
|
||
al_label="${GREEN}enabled${NC}"
|
||
fi
|
||
echo -e " ${DIM}Auto-listen: ${NC}${al_label}"
|
||
|
||
local ptt_display="SPACE"
|
||
[ "$PTT_KEY" != " " ] && ptt_display="$PTT_KEY"
|
||
echo -e " ${DIM}PTT key: ${NC}${WHITE}${ptt_display}${NC}"
|
||
|
||
local vfx_display="${VOICE_EFFECT}"
|
||
[ "$vfx_display" = "none" ] && vfx_display="off"
|
||
echo -e " ${DIM}Voice effect: ${NC}${WHITE}${vfx_display}${NC}"
|
||
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
local vp_label="${RED}disabled${NC}"
|
||
if [ "$VOL_PTT" -eq 1 ]; then
|
||
vp_label="${GREEN}enabled${NC}"
|
||
fi
|
||
echo -e " ${DIM}Volume PTT: ${NC}${vp_label} ${DIM}(experimental)${NC}"
|
||
fi
|
||
echo ""
|
||
|
||
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}3${NC} ${CYAN}│${NC} Snowflake bridge (censorship circumvention)"
|
||
echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Auto-listen (listen for calls automatically once Tor starts)"
|
||
echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Change PTT (push-to-talk) key"
|
||
echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Voice changer"
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
echo -e " ${BOLD}${WHITE}7${NC} ${CYAN}│${NC} Volume PTT ${DIM}(double-tap Vol Down to talk, experimental)${NC}"
|
||
fi
|
||
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 ;;
|
||
3) settings_snowflake ;;
|
||
4)
|
||
if [ "$AUTO_LISTEN" -eq 1 ]; then
|
||
AUTO_LISTEN=0
|
||
stop_auto_listener
|
||
log_ok "Auto-listen disabled"
|
||
else
|
||
AUTO_LISTEN=1
|
||
log_ok "Auto-listen enabled"
|
||
start_auto_listener
|
||
fi
|
||
save_config
|
||
sleep 1
|
||
;;
|
||
5)
|
||
local _pd="SPACE"
|
||
[ "$PTT_KEY" != " " ] && _pd="$PTT_KEY"
|
||
echo -e "\n ${DIM}Current PTT key: ${NC}${WHITE}${_pd}${NC}"
|
||
echo -ne " ${BOLD}Press the key you want to use for PTT: ${NC}"
|
||
# Read a single character in raw mode
|
||
local _old_stty
|
||
_old_stty=$(stty -g)
|
||
stty raw -echo
|
||
local _newkey
|
||
_newkey=$(dd bs=1 count=1 2>/dev/null) || true
|
||
stty "$_old_stty"
|
||
if [ -n "$_newkey" ]; then
|
||
PTT_KEY="$_newkey"
|
||
save_config
|
||
local _nd="SPACE"
|
||
[ "$PTT_KEY" != " " ] && _nd="$PTT_KEY"
|
||
log_ok "PTT key set to: ${_nd}"
|
||
fi
|
||
sleep 1
|
||
;;
|
||
6) settings_voice ;;
|
||
7)
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
if [ "$VOL_PTT" -eq 1 ]; then
|
||
VOL_PTT=0
|
||
log_ok "Volume PTT disabled"
|
||
else
|
||
if ! check_dep jq; then
|
||
echo ""
|
||
echo -ne " ${BOLD}jq is required for Volume PTT. Install now? [Y/n]: ${NC}"
|
||
read -r _jq_confirm
|
||
if [ "$_jq_confirm" != "n" ] && [ "$_jq_confirm" != "N" ]; then
|
||
pkg install -y jq 2>/dev/null || true
|
||
if ! check_dep jq; then
|
||
log_err "jq installation failed — Volume PTT not enabled"
|
||
sleep 2
|
||
continue
|
||
fi
|
||
else
|
||
echo -e "\n ${YELLOW}Volume PTT not enabled (jq not installed)${NC}"
|
||
sleep 2
|
||
continue
|
||
fi
|
||
fi
|
||
VOL_PTT=1
|
||
log_ok "Volume PTT enabled (double-tap Vol Down to toggle recording)"
|
||
echo -e " ${DIM}Note: Each press will lower your device volume.${NC}"
|
||
echo -e " ${DIM}You may want to start with volume at max.${NC}"
|
||
fi
|
||
save_config
|
||
sleep 2
|
||
else
|
||
echo -e "\n ${RED}Volume PTT is only available in Termux${NC}"
|
||
sleep 1
|
||
fi
|
||
;;
|
||
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 2>/dev/null | openssl enc -d -"${selected}" -pbkdf2 -pass pass:test &>/dev/null; then
|
||
CIPHER="$selected"
|
||
save_config
|
||
# Update runtime file for live mid-call sync
|
||
[ -f "$CIPHER_RUNTIME_FILE" ] && echo "$CIPHER" > "$CIPHER_RUNTIME_FILE"
|
||
# Notify remote side if in a call
|
||
if [ "$CALL_ACTIVE" -eq 1 ]; then
|
||
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
|
||
}
|
||
|
||
settings_snowflake() {
|
||
clear
|
||
echo -e "\n${BOLD}${CYAN}═══ Snowflake Bridge ═══${NC}\n"
|
||
echo -e " ${DIM}Snowflake uses WebRTC proxies to help bypass Tor censorship.${NC}"
|
||
echo -e " ${DIM}Enable this if Tor is blocked in your region.${NC}\n"
|
||
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
echo -e " Status: ${GREEN}${BOLD}ENABLED${NC}"
|
||
else
|
||
echo -e " Status: ${RED}${BOLD}DISABLED${NC}"
|
||
fi
|
||
|
||
if check_dep snowflake-client; then
|
||
echo -e " Binary: ${GREEN}●${NC} snowflake-client installed"
|
||
else
|
||
echo -e " Binary: ${RED}●${NC} snowflake-client not installed"
|
||
fi
|
||
|
||
echo -e " ${DIM}Tor manages snowflake-client as a pluggable transport.${NC}"
|
||
|
||
echo ""
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Disable Snowflake"
|
||
else
|
||
echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Enable Snowflake"
|
||
fi
|
||
echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}"
|
||
echo ""
|
||
echo -ne " ${BOLD}Select: ${NC}"
|
||
read -r sf_choice
|
||
|
||
case "$sf_choice" in
|
||
1)
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
SNOWFLAKE_ENABLED=0
|
||
save_config
|
||
|
||
echo -e "\n ${YELLOW}${BOLD}✓${NC} Snowflake disabled"
|
||
echo -e " ${DIM}Restart Tor for changes to take effect.${NC}"
|
||
else
|
||
# Install if not present
|
||
if ! check_dep snowflake-client; then
|
||
echo ""
|
||
echo -ne " ${BOLD}snowflake-client not installed. Install now? [Y/n]: ${NC}"
|
||
read -r install_confirm
|
||
if [ "$install_confirm" != "n" ] && [ "$install_confirm" != "N" ]; then
|
||
install_snowflake || {
|
||
echo -ne " ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
return
|
||
}
|
||
else
|
||
echo -e "\n ${YELLOW}Snowflake not enabled (client not installed)${NC}"
|
||
echo -ne " ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
return
|
||
fi
|
||
fi
|
||
SNOWFLAKE_ENABLED=1
|
||
save_config
|
||
echo -e "\n ${GREEN}${BOLD}✓${NC} Snowflake enabled"
|
||
echo -e " ${DIM}Restart Tor for changes to take effect.${NC}"
|
||
fi
|
||
;;
|
||
0|q|Q) return ;;
|
||
*)
|
||
echo -e "\n ${RED}Invalid choice${NC}"
|
||
;;
|
||
esac
|
||
echo -ne " ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
}
|
||
|
||
settings_voice() {
|
||
echo -e "\n${BOLD}${CYAN}═══ Voice Changer ═══${NC}\n"
|
||
echo -e " ${DIM}Current effect: ${NC}${GREEN}${VOICE_EFFECT}${NC}\n"
|
||
|
||
local effects=("none" "deep" "high" "robot" "echo" "whisper" "custom")
|
||
local descs=(
|
||
"No effect (natural voice)"
|
||
"Deep voice (pitch shifted down)"
|
||
"High voice (pitch shifted up)"
|
||
"Robot (overdrive + flanger)"
|
||
"Echo (delayed reverb)"
|
||
"Whisper (highpass + tremolo)"
|
||
"Custom (configure all parameters)"
|
||
)
|
||
|
||
local i
|
||
for i in "${!effects[@]}"; do
|
||
local num=$(( i + 1 ))
|
||
local marker=" "
|
||
if [ "${effects[$i]}" = "$VOICE_EFFECT" ]; then
|
||
marker="${GREEN}> ${NC}"
|
||
fi
|
||
echo -e " ${marker}${BOLD}${WHITE}${num}${NC} ${CYAN}│${NC} ${descs[$i]}"
|
||
done
|
||
|
||
echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Back${NC}"
|
||
echo ""
|
||
echo -ne " ${BOLD}Select: ${NC}"
|
||
read -r vchoice
|
||
|
||
case "$vchoice" in
|
||
1) VOICE_EFFECT="none" ;;
|
||
2) VOICE_EFFECT="deep" ;;
|
||
3) VOICE_EFFECT="high" ;;
|
||
4) VOICE_EFFECT="robot" ;;
|
||
5) VOICE_EFFECT="echo" ;;
|
||
6) VOICE_EFFECT="whisper" ;;
|
||
7) VOICE_EFFECT="custom"
|
||
settings_voice_custom
|
||
;;
|
||
0|q|Q) return ;;
|
||
*)
|
||
echo -e "\n ${RED}Invalid choice${NC}"
|
||
sleep 1
|
||
return
|
||
;;
|
||
esac
|
||
save_config
|
||
log_ok "Voice effect set to: ${VOICE_EFFECT}"
|
||
sleep 1
|
||
}
|
||
|
||
settings_voice_custom() {
|
||
while true; do
|
||
clear
|
||
echo -e "\n${BOLD}${CYAN}═══ Custom Voice Effect ═══${NC}\n"
|
||
echo -e " ${DIM}Configure each parameter. Effects are combined into one chain.${NC}"
|
||
echo -e " ${DIM}Set a value to 0 to disable that effect.${NC}\n"
|
||
|
||
local _p_status="${RED}off${NC}"
|
||
[ "$VOICE_PITCH" -ne 0 ] 2>/dev/null && _p_status="${GREEN}${VOICE_PITCH} cents${NC}"
|
||
local _od_status="${RED}off${NC}"
|
||
[ "$VOICE_OVERDRIVE" -gt 0 ] 2>/dev/null && _od_status="${GREEN}${VOICE_OVERDRIVE}${NC}"
|
||
local _fl_status="${RED}off${NC}"
|
||
[ "$VOICE_FLANGER" -eq 1 ] 2>/dev/null && _fl_status="${GREEN}on${NC}"
|
||
local _ed_status="${RED}off${NC}"
|
||
[ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null && _ed_status="${GREEN}${VOICE_ECHO_DELAY}ms decay 0.${VOICE_ECHO_DECAY}${NC}"
|
||
local _hp_status="${RED}off${NC}"
|
||
[ "$VOICE_HIGHPASS" -gt 0 ] 2>/dev/null && _hp_status="${GREEN}${VOICE_HIGHPASS} Hz${NC}"
|
||
local _tr_status="${RED}off${NC}"
|
||
[ "$VOICE_TREMOLO" -gt 0 ] 2>/dev/null && _tr_status="${GREEN}${VOICE_TREMOLO} Hz${NC}"
|
||
|
||
echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Pitch shift ${_p_status} ${DIM}(-600 to +600 cents)${NC}"
|
||
echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Overdrive ${_od_status} ${DIM}(0=off, 5-20)${NC}"
|
||
echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Flanger ${_fl_status} ${DIM}(0=off, 1=on)${NC}"
|
||
echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Echo ${_ed_status} ${DIM}(delay 0-200ms, decay 1-9)${NC}"
|
||
echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Highpass filter ${_hp_status} ${DIM}(0=off, 300-2000 Hz)${NC}"
|
||
echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Tremolo ${_tr_status} ${DIM}(0=off, 5-40 Hz)${NC}"
|
||
echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${DIM}Done${NC}"
|
||
echo ""
|
||
echo -ne " ${BOLD}Select parameter: ${NC}"
|
||
read -r pchoice
|
||
|
||
case "$pchoice" in
|
||
1)
|
||
echo -ne " ${BOLD}Pitch shift (cents, -600 to +600, 0=off): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^-?[0-9]+$ ]] && VOICE_PITCH=$val
|
||
;;
|
||
2)
|
||
echo -ne " ${BOLD}Overdrive gain (0=off, 5-20): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^[0-9]+$ ]] && VOICE_OVERDRIVE=$val
|
||
;;
|
||
3)
|
||
echo -ne " ${BOLD}Flanger (0=off, 1=on): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^[01]$ ]] && VOICE_FLANGER=$val
|
||
;;
|
||
4)
|
||
echo -ne " ${BOLD}Echo delay (ms, 0=off, 20-200): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^[0-9]+$ ]] && VOICE_ECHO_DELAY=$val
|
||
if [ "$VOICE_ECHO_DELAY" -gt 0 ] 2>/dev/null; then
|
||
echo -ne " ${BOLD}Echo decay (1-9, maps to 0.1-0.9): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^[1-9]$ ]] && VOICE_ECHO_DECAY=$val
|
||
fi
|
||
;;
|
||
5)
|
||
echo -ne " ${BOLD}Highpass frequency (Hz, 0=off, 300-2000): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^[0-9]+$ ]] && VOICE_HIGHPASS=$val
|
||
;;
|
||
6)
|
||
echo -ne " ${BOLD}Tremolo speed (Hz, 0=off, 5-40): ${NC}"
|
||
read -r val
|
||
[[ "$val" =~ ^[0-9]+$ ]] && VOICE_TREMOLO=$val
|
||
;;
|
||
0|q|Q)
|
||
save_config
|
||
return
|
||
;;
|
||
*)
|
||
echo -e "\n ${RED}Invalid choice${NC}"
|
||
sleep 1
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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
|
||
local sf_status="${RED}●${NC}"
|
||
if [ "$SNOWFLAKE_ENABLED" -eq 1 ]; then
|
||
sf_status="${GREEN}●${NC}"
|
||
fi
|
||
local al_status="${RED}●${NC}"
|
||
if [ "$AUTO_LISTEN" -eq 1 ]; then
|
||
al_status="${GREEN}●${NC}"
|
||
fi
|
||
local _ptt_d="SPACE"
|
||
[ "$PTT_KEY" != " " ] && _ptt_d="$PTT_KEY"
|
||
|
||
echo -e " ${DIM}Tor:${NC} $tor_status ${DIM}Secret:${NC} $secret_status ${DIM}SF:${NC} $sf_status ${DIM}AL:${NC} $al_status ${DIM}PTT:${NC} ${GREEN}[${_ptt_d}]${NC}\n"
|
||
|
||
echo -e " ${BOLD}${WHITE}1${NC} ${CYAN}│${NC} Listen for calls"
|
||
echo -e " ${BOLD}${WHITE}2${NC} ${CYAN}│${NC} Call an onion address"
|
||
echo -e " ${BOLD}${WHITE}3${NC} ${CYAN}│${NC} Show my onion address"
|
||
echo -e " ${BOLD}${WHITE}4${NC} ${CYAN}│${NC} Set shared secret"
|
||
echo -e " ${BOLD}${WHITE}5${NC} ${CYAN}│${NC} Test audio (loopback)"
|
||
echo -e " ${BOLD}${WHITE}6${NC} ${CYAN}│${NC} Show status"
|
||
echo -e " ${BOLD}${WHITE}7${NC} ${CYAN}│${NC} Install dependencies"
|
||
echo -e " ${BOLD}${WHITE}8${NC} ${CYAN}│${NC} Start Tor"
|
||
echo -e " ${BOLD}${WHITE}9${NC} ${CYAN}│${NC} Stop Tor"
|
||
echo -e " ${BOLD}${WHITE}10${NC}${CYAN}│${NC} Restart Tor"
|
||
echo -e " ${BOLD}${WHITE}11${NC}${CYAN}│${NC} Rotate onion address"
|
||
echo -e " ${BOLD}${WHITE}12${NC}${CYAN}│${NC} Settings"
|
||
echo -e " ${BOLD}${WHITE}0${NC} ${CYAN}│${NC} ${RED}Quit${NC}"
|
||
echo ""
|
||
echo -ne " ${BOLD}Select: ${NC}"
|
||
|
||
# If auto-listen is active, poll for incoming calls without redrawing
|
||
local choice=""
|
||
if [ "$AUTO_LISTEN" -eq 1 ] && [ -n "$AUTO_LISTEN_PID" ]; then
|
||
echo -ne "${DIM}[Auto-listening...]${NC} " >&2
|
||
while true; do
|
||
# Check for incoming call
|
||
if check_auto_listen; then
|
||
choice=""
|
||
break
|
||
fi
|
||
# Try to read user input with short timeout
|
||
if read -r -t 1 choice 2>/dev/null; then
|
||
break # user typed something
|
||
fi
|
||
done
|
||
else
|
||
read -r choice
|
||
fi
|
||
|
||
[ -z "$choice" ] && continue
|
||
|
||
case "$choice" in
|
||
1) listen_for_call ;;
|
||
2) call_remote ;;
|
||
3)
|
||
local onion
|
||
onion=$(get_onion)
|
||
if [ -n "$onion" ]; then
|
||
echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n"
|
||
|
||
# QR code generation
|
||
if check_dep qrencode; then
|
||
echo -ne " ${BOLD}Show QR code? [Y/n]: ${NC}"
|
||
read -r _qr_show
|
||
if [ "$_qr_show" != "n" ] && [ "$_qr_show" != "N" ]; then
|
||
tput smcup 2>/dev/null || true
|
||
clear
|
||
echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n"
|
||
qrencode -t ANSIUTF8 "$onion"
|
||
echo ""
|
||
echo -ne " ${DIM}Press Enter to dismiss QR code...${NC}"
|
||
read -r
|
||
tput rmcup 2>/dev/null || true
|
||
fi
|
||
else
|
||
echo -ne " ${BOLD}Install QR code generator (qrencode) to display a scannable QR? [Y/n]: ${NC}"
|
||
read -r _qr_install
|
||
if [ "$_qr_install" != "n" ] && [ "$_qr_install" != "N" ]; then
|
||
local SUDO="sudo"
|
||
if [ $IS_TERMUX -eq 1 ]; then
|
||
SUDO=""
|
||
pkg install -y libqrencode 2>/dev/null
|
||
elif check_dep apt-get; then
|
||
$SUDO apt-get install -y qrencode 2>/dev/null
|
||
elif check_dep dnf; then
|
||
$SUDO dnf install -y qrencode 2>/dev/null
|
||
elif check_dep pacman; then
|
||
$SUDO pacman -S --noconfirm qrencode 2>/dev/null
|
||
else
|
||
log_err "No supported package manager found. Install qrencode manually."
|
||
fi
|
||
|
||
if check_dep qrencode; then
|
||
log_ok "qrencode installed successfully!"
|
||
sleep 1
|
||
tput smcup 2>/dev/null || true
|
||
clear
|
||
echo -e "\n ${BOLD}${GREEN}Your address:${NC} ${WHITE}${BOLD}${onion}${NC}\n"
|
||
qrencode -t ANSIUTF8 "$onion"
|
||
echo ""
|
||
echo -ne " ${DIM}Press Enter to dismiss QR code...${NC}"
|
||
read -r
|
||
tput rmcup 2>/dev/null || true
|
||
else
|
||
log_err "qrencode installation failed."
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
echo -e "\n ${YELLOW}Tor hidden service not running. Start Tor first (option 8).${NC}\n"
|
||
fi
|
||
echo -ne " ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
4) set_shared_secret ;;
|
||
5) test_audio
|
||
echo -ne " ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
6) show_status
|
||
echo -ne " ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
7) install_deps
|
||
echo -ne "\n ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
8)
|
||
start_tor
|
||
start_auto_listener
|
||
echo -ne "\n ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
9)
|
||
stop_tor
|
||
echo -ne "\n ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
10)
|
||
stop_tor
|
||
start_tor
|
||
start_auto_listener
|
||
echo -ne "\n ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
11)
|
||
rotate_onion
|
||
echo -ne "\n ${DIM}Press Enter to continue...${NC}"
|
||
read -r
|
||
;;
|
||
12)
|
||
settings_menu
|
||
;;
|
||
0|q|Q)
|
||
echo -e "\n${GREEN}Goodbye!${NC}"
|
||
stop_tor
|
||
cleanup
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo -e "\n ${RED}Invalid choice${NC}"
|
||
sleep 1
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
#=============================================================================
|
||
# 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
|