Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env bash
- # termux-tor-chat.sh
- # Install and run with nano chat.sh > save this script > chmod +x chat.sh > ./chat.sh
- # This has tons of issues that im still working on. Give it a second on initial connection.
- # I have only tested on 3 devices your results may vary.
- # Keeping existing onion address is broken in this version you have to rotate everytime
- # Only clients who enter the same shared password will be able to see each others messages.
- # If you are connected with a different password you will see <encrypted message (failed integrity)>
- set -euo pipefail
- SCRIPT_DIR="$HOME/.termux_chat"
- PY_SERVER="$SCRIPT_DIR/server.py"
- PY_CLIENT="$SCRIPT_DIR/client.py"
- TORRC="$SCRIPT_DIR/torrc"
- HS_BASE="$SCRIPT_DIR/hidden_services"
- HS_CURRENT="$HS_BASE/current"
- DEFAULT_PORT=9001
- LOCAL_BIND=127.0.0.1
- SERVER_PORT=${SERVER_PORT:-$DEFAULT_PORT}
- SERVER_BIND=${SERVER_BIND:-0.0.0.0}
- mkdir -p "$SCRIPT_DIR" "$HS_BASE" "$HS_CURRENT"
- command_exists() { command -v "$1" >/dev/null 2>&1; }
- auto_install_deps() {
- local deps=("tor" "python" "dialog" "socat" "torsocks" "netcat-openbsd")
- local missing=()
- for pkg in "${deps[@]}"; do
- if ! command_exists "$pkg"; then
- missing+=("$pkg")
- fi
- done
- if [ ${#missing[@]} -ne 0 ]; then
- echo "[*] Installing missing dependencies: ${missing[*]}"
- if command_exists pkg; then
- pkg update -y
- pkg install -y "${missing[@]}"
- elif command_exists apt-get; then
- apt-get update -y || true
- apt-get install -y "${missing[@]}"
- else
- echo "Please install: ${missing[*]}"
- fi
- fi
- }
- write_torrc() {
- cat >"$TORRC" <<EOF
- Log notice stdout
- RunAsDaemon 0
- SocksPort 9050
- ControlPort 9051
- HiddenServiceDir REPLACE_HS_DIR
- HiddenServiceVersion 3
- HiddenServicePort $SERVER_PORT $LOCAL_BIND:$SERVER_PORT
- EOF
- }
- start_tor_with_hs() {
- local hs_dir="$1"
- sed "s|REPLACE_HS_DIR|$hs_dir|g" "$TORRC" > "$TORRC".active
- if command_exists pkill; then pkill -f "tor -f $TORRC.active" 2>/dev/null || true; fi
- tor -f "$TORRC".active >/dev/null 2>&1 &
- TOR_PID=$!
- local t=0
- while [ ! -f "$hs_dir/hostname" ]; do
- sleep 0.4; t=$((t+1))
- [ $t -gt 100 ] && { echo "Tor timeout."; return 1; }
- done
- echo "Onion: $(cat "$hs_dir/hostname")"
- echo "$TOR_PID"
- }
- # ================= PYTHON SERVER =================
- cat > "$PY_SERVER" <<'PYSERVER'
- #!/usr/bin/env python3
- import socket, threading, sys
- HOST="0.0.0.0"
- PORT=int(sys.argv[1]) if len(sys.argv)>1 else 9001
- lock=threading.Lock()
- clients={} # conn -> {"nick":str}
- def send(conn,msg):
- try: conn.sendall(msg.encode()+b"\n")
- except: pass
- def broadcast(msg, exclude=None):
- with lock:
- for c in list(clients.keys()):
- if c==exclude: continue
- try: c.sendall(msg.encode()+b"\n")
- except:
- try:c.close()
- except:pass
- remove_client(c)
- def remove_client(conn):
- info=clients.get(conn);
- if not info: return
- nick=info.get("nick","anon")
- try: del clients[conn]
- except: pass
- broadcast(f"** {nick} left chat **")
- send_user_list()
- def send_user_list():
- with lock:
- nicks=[clients[c]["nick"] for c in clients]
- for c in list(clients.keys()):
- send(c,"[USERS] "+",".join(nicks))
- def handle(conn,addr):
- with lock:
- clients[conn]={"nick":"anon"}
- send(conn,"Welcome. Use /nick NAME, /quit")
- broadcast("** anon joined chat **", exclude=conn)
- send_user_list()
- buf=b""
- try:
- while True:
- data=conn.recv(4096)
- if not data: break
- buf+=data
- while b"\n" in buf:
- line,buf=buf.split(b"\n",1)
- line=line.decode(errors="ignore").strip()
- if not line: continue
- info=clients.get(conn);
- if not info: continue
- nick=info["nick"]
- if line.startswith("/nick "):
- new=line.split(" ",1)[1].strip() or "anon"
- old=nick; info["nick"]=new
- broadcast(f"** {old} is now known as {new} **")
- send_user_list()
- elif line=="/who":
- nicks=[clients[c]["nick"] for c in clients]
- send(conn,"[SERVER] Users: "+", ".join(nicks))
- elif line=="/quit":
- return
- elif line.startswith("/"):
- send(conn,"[SERVER] Unknown command")
- else:
- if ":" in line and line.split(":",1)[0].strip() != "":
- fullmsg=line
- else:
- fullmsg=f"{nick}: {line}"
- broadcast(fullmsg, exclude=None)
- finally:
- remove_client(conn)
- try: conn.close()
- except: pass
- def main():
- s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
- s.bind((HOST,PORT)); s.listen(50)
- print("Server listening on",PORT)
- try:
- while True:
- conn,addr=s.accept()
- threading.Thread(target=handle,args=(conn,addr),daemon=True).start()
- except KeyboardInterrupt: pass
- finally: s.close()
- if __name__=="__main__": main()
- PYSERVER
- chmod +x "$PY_SERVER"
- # ================= PYTHON CLIENT =================
- cat > "$PY_CLIENT" <<'PYCLIENT'
- #!/usr/bin/env python3
- import curses,socket,threading,time,sys,select,hashlib,hmac,os,struct,textwrap
- HOST="127.0.0.1"
- PORT=int(sys.argv[1]) if len(sys.argv)>1 else 9001
- nick=""; sock=None; stop_event=threading.Event()
- messages=[]; users=[]
- def derive_key(onion, password):
- return hmac.new(onion.encode(), password.encode(), hashlib.sha256).digest()
- def encrypt(key, plaintext):
- nonce=os.urandom(8)
- out=bytearray()
- block_counter=0
- i=0
- while i < len(plaintext):
- ctr_bytes=struct.pack('>Q', block_counter)
- block=hmac.new(key, nonce+ctr_bytes, hashlib.sha256).digest()
- take=min(len(block), len(plaintext)-i)
- for j in range(take):
- out.append(plaintext[i+j] ^ block[j])
- i+=take; block_counter+=1
- tag=hmac.new(key, nonce+bytes(out), hashlib.sha256).digest()
- return "ENC:" + nonce.hex() + ":" + out.hex() + ":" + tag.hex()
- def decrypt(key, encstr):
- try:
- _,nonce_hex,ct_hex,tag_hex = encstr.split(":",3)
- nonce=bytes.fromhex(nonce_hex); ct=bytes.fromhex(ct_hex); tag=bytes.fromhex(tag_hex)
- except Exception:
- return None
- expected=hmac.new(key, nonce+ct, hashlib.sha256).digest()
- if not hmac.compare_digest(expected, tag):
- return None
- out=bytearray(); block_counter=0; i=0
- while i < len(ct):
- ctr_bytes=struct.pack('>Q', block_counter)
- block=hmac.new(key, nonce+ctr_bytes, hashlib.sha256).digest()
- take=min(len(block), len(ct)-i)
- for j in range(take):
- out.append(ct[i+j] ^ block[j])
- i+=take; block_counter+=1
- return out.decode(errors='ignore')
- def receiver(key):
- global messages,users
- while not stop_event.is_set():
- try:
- r,_,_=select.select([sock],[],[],0.5)
- if sock in r:
- data=sock.recv(4096)
- if not data: stop_event.set(); break
- for line in data.decode(errors="ignore").splitlines():
- if line.startswith("[USERS]"):
- users=[u for u in line.split(" ",1)[1].split(",") if u]
- elif line.startswith("[SERVER]"):
- messages.append(line)
- else:
- if key and line.startswith("ENC:"):
- dec=decrypt(key,line)
- if dec is None:
- messages.append("<encrypted message (failed integrity)>")
- else:
- messages.append(dec)
- else:
- messages.append(line)
- if len(messages)>1000: messages[:]=messages[-1000:]
- except:
- stop_event.set(); break
- def draw_ui(stdscr):
- curses.curs_set(1); stdscr.nodelay(True); stdscr.keypad(True)
- sh,sw=stdscr.getmaxyx()
- header_h,input_h=3,3
- users_w=max(20,int(sw*0.20)); chat_w=sw-users_w-2
- chat_h=sh-header_h-input_h-2
- header=curses.newwin(header_h,sw,0,0)
- chat=curses.newwin(chat_h,chat_w,header_h,0)
- users_win=curses.newwin(chat_h,users_w,header_h,chat_w+2)
- input_win=curses.newwin(input_h,sw,header_h+chat_h+1,0)
- buf=""
- scroll_offset=0
- while not stop_event.is_set():
- header.erase(); header.border()
- header.addstr(1,2,f" Termux Tor Chat | Nick: {nick} ")
- header.refresh()
- users_win.erase(); users_win.border(); users_win.addstr(0,2," Users ")
- for i,u in enumerate(users[:chat_h-2]):
- try: users_win.addstr(1+i,1," "+u[:users_w-3])
- except: pass
- users_win.refresh()
- chat.erase(); chat.border(); chat.addstr(0,2," Chat ")
- total=len(messages)
- visible=chat_h-2
- start=max(0, total-visible-scroll_offset)
- end=start+visible
- slice_msgs=messages[start:end]
- i=0
- for m in slice_msgs:
- wrapped=textwrap.wrap(m, chat_w-2) or [""]
- for line in wrapped:
- if i>=visible: break
- try: chat.addstr(1+i,1,line)
- except: pass
- i+=1
- chat.refresh()
- input_win.erase(); input_win.border(); input_win.addstr(0,2," Type (/quit to exit) ")
- input_win.addstr(1,2,"> "+buf); input_win.refresh()
- try:
- ch=stdscr.getch()
- if ch==-1: time.sleep(0.05); continue
- elif ch in (10,13):
- line=buf.rstrip('\n'); buf=""
- if line:
- if line.startswith("/"):
- sock.sendall((line+"\n").encode())
- else:
- fullmsg=f"{nick}: {line}"
- if KEY:
- enc=encrypt(KEY,fullmsg.encode())
- sock.sendall((enc+"\n").encode())
- else:
- sock.sendall((fullmsg+"\n").encode())
- if line=="/quit": stop_event.set(); break
- scroll_offset=0
- elif ch in (127,curses.KEY_BACKSPACE): buf=buf[:-1]
- elif ch==curses.KEY_PPAGE: scroll_offset = min(max(0, scroll_offset + (chat_h//2)), max(0, len(messages)-(chat_h-2)))
- elif ch==curses.KEY_NPAGE: scroll_offset = max(0, scroll_offset - (chat_h//2))
- elif ch==curses.KEY_UP: scroll_offset = min(max(0, scroll_offset + 1), max(0, len(messages)-(chat_h-2)))
- elif ch==curses.KEY_DOWN: scroll_offset = max(0, scroll_offset - 1)
- elif 0<=ch<256: buf+=chr(ch)
- except: pass
- def main():
- global sock,nick,KEY
- if len(sys.argv)>=4:
- nick=sys.argv[2]
- info=sys.argv[3]
- if '|' in info:
- onion, password = info.split('|',1)
- else:
- onion=""; password=info
- else:
- nick=sys.argv[2] if len(sys.argv)>=3 else input("Nick: ") or "anon"
- onion=input("Onion (e.g. abcdef.onion): ") or ""
- password=input("Shared password (for E2EE): ") or ""
- sock=socket.socket(); sock.connect((HOST,PORT))
- if onion and password:
- KEY=derive_key(onion,password)
- else:
- KEY=None
- threading.Thread(target=receiver,args=(KEY,),daemon=True).start()
- sock.sendall(f"/nick {nick}\n".encode())
- try:
- curses.wrapper(draw_ui)
- finally:
- stop_event.set()
- try: sock.close()
- except: pass
- if __name__=="__main__": main()
- PYCLIENT
- chmod +x "$PY_CLIENT"
- # ===== Menu and flows (unchanged) =====
- main_menu() {
- if command_exists dialog; then
- CHOICE=$(dialog --backtitle "Termux Tor Chat" --menu "Host or Join?" 12 60 3 \
- 1 "Host a server" 2 "Join a server" 3 "Quit" 3>&1 1>&2 2>&3)
- clear
- else
- echo "1) Host"
- echo "2) Join"
- echo "3) Quit"
- read -rp "Choose: " CHOICE
- fi
- case "$CHOICE" in 1) host_flow;; 2) join_flow;; *) exit 0;; esac
- }
- host_flow() {
- if [ -f "$HS_CURRENT/hostname" ]; then
- EXIST_ONION=$(cat "$HS_CURRENT/hostname")
- echo "Existing onion detected: $EXIST_ONION"
- echo "1) Keep existing onion"
- echo "2) Create a new onion (rotate)"
- read -rp "Choose (1/2): " KEEP
- if [ "$KEEP" = "1" ]; then
- HS_DIR="$HS_CURRENT"
- else
- HS_DIR="$HS_BASE/hs_$(date +%s)"; mkdir -p "$HS_DIR"
- fi
- else
- HS_DIR="$HS_BASE/hs_$(date +%s)"; mkdir -p "$HS_DIR"
- fi
- write_torrc; TOR_PID_AND_ONION=$(start_tor_with_hs "$HS_DIR") || { echo "Failed to start Tor"; return 1; }
- TOR_PID=$(echo "$TOR_PID_AND_ONION" | tail -n1)
- mkdir -p "$HS_CURRENT"; rm -rf "$HS_CURRENT"/* || true; cp -r "$HS_DIR/." "$HS_CURRENT/" || true
- python3 "$PY_SERVER" "$SERVER_PORT" &
- SERVER_PID=$!
- echo "Server PID: $SERVER_PID"
- host_menu "$HS_DIR" "$SERVER_PID" "$TOR_PID"
- }
- host_menu() {
- HS_DIR="$1"; SERVER_PID="$2"; TOR_PID="$3"
- while true; do
- if [ -f "$HS_DIR/hostname" ]; then
- ONION_DISPLAY=$(cat "$HS_DIR/hostname")
- else
- ONION_DISPLAY="(not available)"
- fi
- echo "Onion: $ONION_DISPLAY"
- echo "1) Show onion"
- echo "2) Stop server"
- echo "3) Quit"
- read -rp "> " CH
- case "$CH" in
- 1) [ -f "$HS_DIR/hostname" ] && cat "$HS_DIR/hostname" || echo "(no hostname)";;
- 2) kill "$SERVER_PID" 2>/dev/null || true; return;;
- 3) kill "$SERVER_PID" 2>/dev/null || true; kill "$TOR_PID" 2>/dev/null || true; exit 0;;
- esac
- done
- }
- join_flow() {
- read -rp "Onion address (e.g. abcdef.onion): " ONION
- read -rp "Shared password (for E2EE): " PASSWORD
- LOCAL_PORT=$(shuf -i 20000-65000 -n 1)
- socat TCP-LISTEN:$LOCAL_PORT,fork SOCKS4A:127.0.0.1:$ONION:$SERVER_PORT,socksport=9050 &
- FWD=$!; sleep 1
- read -rp "Nick: " NICK
- python3 "$PY_CLIENT" "$LOCAL_PORT" "$NICK" "$ONION|$PASSWORD"
- kill "$FWD" 2>/dev/null || true
- }
- trap 'pkill -P $$ 2>/dev/null || true' EXIT
- auto_install_deps
- main_menu
Add Comment
Please, Sign In to add comment