Guest User

Termux Tor Chat

a guest
Oct 3rd, 2025
269
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.25 KB | None | 0 0
  1. #!/usr/bin/env bash
  2. # termux-tor-chat.sh
  3. # Install and run with nano chat.sh > save this script > chmod +x chat.sh > ./chat.sh
  4. # This has tons of issues that im still working on. Give it a second on initial connection.
  5. # I have only tested on 3 devices your results may vary.
  6. # Keeping existing onion address is broken in this version you have to rotate everytime
  7. # Only clients who enter the same shared password will be able to see each others messages.
  8. # If you are connected with a different password you will see <encrypted message (failed integrity)>
  9.  
  10. set -euo pipefail
  11.  
  12. SCRIPT_DIR="$HOME/.termux_chat"
  13. PY_SERVER="$SCRIPT_DIR/server.py"
  14. PY_CLIENT="$SCRIPT_DIR/client.py"
  15. TORRC="$SCRIPT_DIR/torrc"
  16. HS_BASE="$SCRIPT_DIR/hidden_services"
  17. HS_CURRENT="$HS_BASE/current"
  18. DEFAULT_PORT=9001
  19. LOCAL_BIND=127.0.0.1
  20. SERVER_PORT=${SERVER_PORT:-$DEFAULT_PORT}
  21. SERVER_BIND=${SERVER_BIND:-0.0.0.0}
  22.  
  23. mkdir -p "$SCRIPT_DIR" "$HS_BASE" "$HS_CURRENT"
  24.  
  25. command_exists() { command -v "$1" >/dev/null 2>&1; }
  26.  
  27. auto_install_deps() {
  28. local deps=("tor" "python" "dialog" "socat" "torsocks" "netcat-openbsd")
  29. local missing=()
  30. for pkg in "${deps[@]}"; do
  31. if ! command_exists "$pkg"; then
  32. missing+=("$pkg")
  33. fi
  34. done
  35. if [ ${#missing[@]} -ne 0 ]; then
  36. echo "[*] Installing missing dependencies: ${missing[*]}"
  37. if command_exists pkg; then
  38. pkg update -y
  39. pkg install -y "${missing[@]}"
  40. elif command_exists apt-get; then
  41. apt-get update -y || true
  42. apt-get install -y "${missing[@]}"
  43. else
  44. echo "Please install: ${missing[*]}"
  45. fi
  46. fi
  47. }
  48.  
  49. write_torrc() {
  50. cat >"$TORRC" <<EOF
  51. Log notice stdout
  52. RunAsDaemon 0
  53. SocksPort 9050
  54. ControlPort 9051
  55. HiddenServiceDir REPLACE_HS_DIR
  56. HiddenServiceVersion 3
  57. HiddenServicePort $SERVER_PORT $LOCAL_BIND:$SERVER_PORT
  58. EOF
  59. }
  60.  
  61. start_tor_with_hs() {
  62. local hs_dir="$1"
  63. sed "s|REPLACE_HS_DIR|$hs_dir|g" "$TORRC" > "$TORRC".active
  64. if command_exists pkill; then pkill -f "tor -f $TORRC.active" 2>/dev/null || true; fi
  65. tor -f "$TORRC".active >/dev/null 2>&1 &
  66. TOR_PID=$!
  67. local t=0
  68. while [ ! -f "$hs_dir/hostname" ]; do
  69. sleep 0.4; t=$((t+1))
  70. [ $t -gt 100 ] && { echo "Tor timeout."; return 1; }
  71. done
  72. echo "Onion: $(cat "$hs_dir/hostname")"
  73. echo "$TOR_PID"
  74. }
  75.  
  76. # ================= PYTHON SERVER =================
  77. cat > "$PY_SERVER" <<'PYSERVER'
  78. #!/usr/bin/env python3
  79. import socket, threading, sys
  80.  
  81. HOST="0.0.0.0"
  82. PORT=int(sys.argv[1]) if len(sys.argv)>1 else 9001
  83.  
  84. lock=threading.Lock()
  85. clients={} # conn -> {"nick":str}
  86.  
  87. def send(conn,msg):
  88. try: conn.sendall(msg.encode()+b"\n")
  89. except: pass
  90.  
  91. def broadcast(msg, exclude=None):
  92. with lock:
  93. for c in list(clients.keys()):
  94. if c==exclude: continue
  95. try: c.sendall(msg.encode()+b"\n")
  96. except:
  97. try:c.close()
  98. except:pass
  99. remove_client(c)
  100.  
  101. def remove_client(conn):
  102. info=clients.get(conn);
  103. if not info: return
  104. nick=info.get("nick","anon")
  105. try: del clients[conn]
  106. except: pass
  107. broadcast(f"** {nick} left chat **")
  108. send_user_list()
  109.  
  110. def send_user_list():
  111. with lock:
  112. nicks=[clients[c]["nick"] for c in clients]
  113. for c in list(clients.keys()):
  114. send(c,"[USERS] "+",".join(nicks))
  115.  
  116. def handle(conn,addr):
  117. with lock:
  118. clients[conn]={"nick":"anon"}
  119. send(conn,"Welcome. Use /nick NAME, /quit")
  120. broadcast("** anon joined chat **", exclude=conn)
  121. send_user_list()
  122.  
  123. buf=b""
  124. try:
  125. while True:
  126. data=conn.recv(4096)
  127. if not data: break
  128. buf+=data
  129. while b"\n" in buf:
  130. line,buf=buf.split(b"\n",1)
  131. line=line.decode(errors="ignore").strip()
  132. if not line: continue
  133. info=clients.get(conn);
  134. if not info: continue
  135. nick=info["nick"]
  136. if line.startswith("/nick "):
  137. new=line.split(" ",1)[1].strip() or "anon"
  138. old=nick; info["nick"]=new
  139. broadcast(f"** {old} is now known as {new} **")
  140. send_user_list()
  141. elif line=="/who":
  142. nicks=[clients[c]["nick"] for c in clients]
  143. send(conn,"[SERVER] Users: "+", ".join(nicks))
  144. elif line=="/quit":
  145. return
  146. elif line.startswith("/"):
  147. send(conn,"[SERVER] Unknown command")
  148. else:
  149. if ":" in line and line.split(":",1)[0].strip() != "":
  150. fullmsg=line
  151. else:
  152. fullmsg=f"{nick}: {line}"
  153. broadcast(fullmsg, exclude=None)
  154. finally:
  155. remove_client(conn)
  156. try: conn.close()
  157. except: pass
  158.  
  159. def main():
  160. s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
  161. s.bind((HOST,PORT)); s.listen(50)
  162. print("Server listening on",PORT)
  163. try:
  164. while True:
  165. conn,addr=s.accept()
  166. threading.Thread(target=handle,args=(conn,addr),daemon=True).start()
  167. except KeyboardInterrupt: pass
  168. finally: s.close()
  169.  
  170. if __name__=="__main__": main()
  171. PYSERVER
  172. chmod +x "$PY_SERVER"
  173.  
  174. # ================= PYTHON CLIENT =================
  175. cat > "$PY_CLIENT" <<'PYCLIENT'
  176. #!/usr/bin/env python3
  177. import curses,socket,threading,time,sys,select,hashlib,hmac,os,struct,textwrap
  178.  
  179. HOST="127.0.0.1"
  180. PORT=int(sys.argv[1]) if len(sys.argv)>1 else 9001
  181. nick=""; sock=None; stop_event=threading.Event()
  182. messages=[]; users=[]
  183.  
  184. def derive_key(onion, password):
  185. return hmac.new(onion.encode(), password.encode(), hashlib.sha256).digest()
  186.  
  187. def encrypt(key, plaintext):
  188. nonce=os.urandom(8)
  189. out=bytearray()
  190. block_counter=0
  191. i=0
  192. while i < len(plaintext):
  193. ctr_bytes=struct.pack('>Q', block_counter)
  194. block=hmac.new(key, nonce+ctr_bytes, hashlib.sha256).digest()
  195. take=min(len(block), len(plaintext)-i)
  196. for j in range(take):
  197. out.append(plaintext[i+j] ^ block[j])
  198. i+=take; block_counter+=1
  199. tag=hmac.new(key, nonce+bytes(out), hashlib.sha256).digest()
  200. return "ENC:" + nonce.hex() + ":" + out.hex() + ":" + tag.hex()
  201.  
  202. def decrypt(key, encstr):
  203. try:
  204. _,nonce_hex,ct_hex,tag_hex = encstr.split(":",3)
  205. nonce=bytes.fromhex(nonce_hex); ct=bytes.fromhex(ct_hex); tag=bytes.fromhex(tag_hex)
  206. except Exception:
  207. return None
  208. expected=hmac.new(key, nonce+ct, hashlib.sha256).digest()
  209. if not hmac.compare_digest(expected, tag):
  210. return None
  211. out=bytearray(); block_counter=0; i=0
  212. while i < len(ct):
  213. ctr_bytes=struct.pack('>Q', block_counter)
  214. block=hmac.new(key, nonce+ctr_bytes, hashlib.sha256).digest()
  215. take=min(len(block), len(ct)-i)
  216. for j in range(take):
  217. out.append(ct[i+j] ^ block[j])
  218. i+=take; block_counter+=1
  219. return out.decode(errors='ignore')
  220.  
  221. def receiver(key):
  222. global messages,users
  223. while not stop_event.is_set():
  224. try:
  225. r,_,_=select.select([sock],[],[],0.5)
  226. if sock in r:
  227. data=sock.recv(4096)
  228. if not data: stop_event.set(); break
  229. for line in data.decode(errors="ignore").splitlines():
  230. if line.startswith("[USERS]"):
  231. users=[u for u in line.split(" ",1)[1].split(",") if u]
  232. elif line.startswith("[SERVER]"):
  233. messages.append(line)
  234. else:
  235. if key and line.startswith("ENC:"):
  236. dec=decrypt(key,line)
  237. if dec is None:
  238. messages.append("<encrypted message (failed integrity)>")
  239. else:
  240. messages.append(dec)
  241. else:
  242. messages.append(line)
  243. if len(messages)>1000: messages[:]=messages[-1000:]
  244. except:
  245. stop_event.set(); break
  246.  
  247. def draw_ui(stdscr):
  248. curses.curs_set(1); stdscr.nodelay(True); stdscr.keypad(True)
  249. sh,sw=stdscr.getmaxyx()
  250. header_h,input_h=3,3
  251. users_w=max(20,int(sw*0.20)); chat_w=sw-users_w-2
  252. chat_h=sh-header_h-input_h-2
  253. header=curses.newwin(header_h,sw,0,0)
  254. chat=curses.newwin(chat_h,chat_w,header_h,0)
  255. users_win=curses.newwin(chat_h,users_w,header_h,chat_w+2)
  256. input_win=curses.newwin(input_h,sw,header_h+chat_h+1,0)
  257.  
  258. buf=""
  259. scroll_offset=0
  260. while not stop_event.is_set():
  261. header.erase(); header.border()
  262. header.addstr(1,2,f" Termux Tor Chat | Nick: {nick} ")
  263. header.refresh()
  264.  
  265. users_win.erase(); users_win.border(); users_win.addstr(0,2," Users ")
  266. for i,u in enumerate(users[:chat_h-2]):
  267. try: users_win.addstr(1+i,1," "+u[:users_w-3])
  268. except: pass
  269. users_win.refresh()
  270.  
  271. chat.erase(); chat.border(); chat.addstr(0,2," Chat ")
  272. total=len(messages)
  273. visible=chat_h-2
  274. start=max(0, total-visible-scroll_offset)
  275. end=start+visible
  276. slice_msgs=messages[start:end]
  277. i=0
  278. for m in slice_msgs:
  279. wrapped=textwrap.wrap(m, chat_w-2) or [""]
  280. for line in wrapped:
  281. if i>=visible: break
  282. try: chat.addstr(1+i,1,line)
  283. except: pass
  284. i+=1
  285. chat.refresh()
  286.  
  287. input_win.erase(); input_win.border(); input_win.addstr(0,2," Type (/quit to exit) ")
  288. input_win.addstr(1,2,"> "+buf); input_win.refresh()
  289.  
  290. try:
  291. ch=stdscr.getch()
  292. if ch==-1: time.sleep(0.05); continue
  293. elif ch in (10,13):
  294. line=buf.rstrip('\n'); buf=""
  295. if line:
  296. if line.startswith("/"):
  297. sock.sendall((line+"\n").encode())
  298. else:
  299. fullmsg=f"{nick}: {line}"
  300. if KEY:
  301. enc=encrypt(KEY,fullmsg.encode())
  302. sock.sendall((enc+"\n").encode())
  303. else:
  304. sock.sendall((fullmsg+"\n").encode())
  305. if line=="/quit": stop_event.set(); break
  306. scroll_offset=0
  307. elif ch in (127,curses.KEY_BACKSPACE): buf=buf[:-1]
  308. elif ch==curses.KEY_PPAGE: scroll_offset = min(max(0, scroll_offset + (chat_h//2)), max(0, len(messages)-(chat_h-2)))
  309. elif ch==curses.KEY_NPAGE: scroll_offset = max(0, scroll_offset - (chat_h//2))
  310. elif ch==curses.KEY_UP: scroll_offset = min(max(0, scroll_offset + 1), max(0, len(messages)-(chat_h-2)))
  311. elif ch==curses.KEY_DOWN: scroll_offset = max(0, scroll_offset - 1)
  312. elif 0<=ch<256: buf+=chr(ch)
  313. except: pass
  314.  
  315. def main():
  316. global sock,nick,KEY
  317. if len(sys.argv)>=4:
  318. nick=sys.argv[2]
  319. info=sys.argv[3]
  320. if '|' in info:
  321. onion, password = info.split('|',1)
  322. else:
  323. onion=""; password=info
  324. else:
  325. nick=sys.argv[2] if len(sys.argv)>=3 else input("Nick: ") or "anon"
  326. onion=input("Onion (e.g. abcdef.onion): ") or ""
  327. password=input("Shared password (for E2EE): ") or ""
  328. sock=socket.socket(); sock.connect((HOST,PORT))
  329. if onion and password:
  330. KEY=derive_key(onion,password)
  331. else:
  332. KEY=None
  333. threading.Thread(target=receiver,args=(KEY,),daemon=True).start()
  334. sock.sendall(f"/nick {nick}\n".encode())
  335. try:
  336. curses.wrapper(draw_ui)
  337. finally:
  338. stop_event.set()
  339. try: sock.close()
  340. except: pass
  341.  
  342. if __name__=="__main__": main()
  343. PYCLIENT
  344. chmod +x "$PY_CLIENT"
  345.  
  346. # ===== Menu and flows (unchanged) =====
  347.  
  348. main_menu() {
  349. if command_exists dialog; then
  350. CHOICE=$(dialog --backtitle "Termux Tor Chat" --menu "Host or Join?" 12 60 3 \
  351. 1 "Host a server" 2 "Join a server" 3 "Quit" 3>&1 1>&2 2>&3)
  352. clear
  353. else
  354. echo "1) Host"
  355. echo "2) Join"
  356. echo "3) Quit"
  357. read -rp "Choose: " CHOICE
  358. fi
  359. case "$CHOICE" in 1) host_flow;; 2) join_flow;; *) exit 0;; esac
  360. }
  361.  
  362. host_flow() {
  363. if [ -f "$HS_CURRENT/hostname" ]; then
  364. EXIST_ONION=$(cat "$HS_CURRENT/hostname")
  365. echo "Existing onion detected: $EXIST_ONION"
  366. echo "1) Keep existing onion"
  367. echo "2) Create a new onion (rotate)"
  368. read -rp "Choose (1/2): " KEEP
  369. if [ "$KEEP" = "1" ]; then
  370. HS_DIR="$HS_CURRENT"
  371. else
  372. HS_DIR="$HS_BASE/hs_$(date +%s)"; mkdir -p "$HS_DIR"
  373. fi
  374. else
  375. HS_DIR="$HS_BASE/hs_$(date +%s)"; mkdir -p "$HS_DIR"
  376. fi
  377.  
  378. write_torrc; TOR_PID_AND_ONION=$(start_tor_with_hs "$HS_DIR") || { echo "Failed to start Tor"; return 1; }
  379. TOR_PID=$(echo "$TOR_PID_AND_ONION" | tail -n1)
  380. mkdir -p "$HS_CURRENT"; rm -rf "$HS_CURRENT"/* || true; cp -r "$HS_DIR/." "$HS_CURRENT/" || true
  381.  
  382. python3 "$PY_SERVER" "$SERVER_PORT" &
  383. SERVER_PID=$!
  384. echo "Server PID: $SERVER_PID"
  385. host_menu "$HS_DIR" "$SERVER_PID" "$TOR_PID"
  386. }
  387.  
  388. host_menu() {
  389. HS_DIR="$1"; SERVER_PID="$2"; TOR_PID="$3"
  390. while true; do
  391. if [ -f "$HS_DIR/hostname" ]; then
  392. ONION_DISPLAY=$(cat "$HS_DIR/hostname")
  393. else
  394. ONION_DISPLAY="(not available)"
  395. fi
  396. echo "Onion: $ONION_DISPLAY"
  397. echo "1) Show onion"
  398. echo "2) Stop server"
  399. echo "3) Quit"
  400. read -rp "> " CH
  401. case "$CH" in
  402. 1) [ -f "$HS_DIR/hostname" ] && cat "$HS_DIR/hostname" || echo "(no hostname)";;
  403. 2) kill "$SERVER_PID" 2>/dev/null || true; return;;
  404. 3) kill "$SERVER_PID" 2>/dev/null || true; kill "$TOR_PID" 2>/dev/null || true; exit 0;;
  405. esac
  406. done
  407. }
  408.  
  409. join_flow() {
  410. read -rp "Onion address (e.g. abcdef.onion): " ONION
  411. read -rp "Shared password (for E2EE): " PASSWORD
  412. LOCAL_PORT=$(shuf -i 20000-65000 -n 1)
  413. socat TCP-LISTEN:$LOCAL_PORT,fork SOCKS4A:127.0.0.1:$ONION:$SERVER_PORT,socksport=9050 &
  414. FWD=$!; sleep 1
  415. read -rp "Nick: " NICK
  416. python3 "$PY_CLIENT" "$LOCAL_PORT" "$NICK" "$ONION|$PASSWORD"
  417. kill "$FWD" 2>/dev/null || true
  418. }
  419.  
  420. trap 'pkill -P $$ 2>/dev/null || true' EXIT
  421.  
  422. auto_install_deps
  423. main_menu
  424.  
Add Comment
Please, Sign In to add comment