
보안 터미널 채팅. E2E 암호화, 제로 메타데이터 블라인드 포워더 서버. PyNaCl XSalsa20-Poly1305 + Ed25519 + 순방향 비밀성. 크로스 플랫폼 Python.
종단 간 암호화된 그룹 채팅, 개인 메시지, 파일 전송을 터미널에서 누리세요. 서버는 블라인드 포워더입니다. 서버는 메시지를 읽을 수 없고, 사용자 이름을 알 수 없으며, 참여 중인 룸도 알 수 없습니다. 설령 서버가 완전히 침해당하더라도 두 메시지를 같은 사람에게 연결할 수 없습니다.
https://github.com/user-attachments/assets/d9faabfb-73bd-46dd-92b2-23f63daf5b06
https://github.com/user-attachments/assets/e8e0220d-cd7d-45a2-9443-9a5f20b57f12
https://github.com/user-attachments/assets/15fb383d-a02a-433e-bbd9-8ebadecf9481
https://github.com/user-attachments/assets/bca10cb1-6959-425d-96d6-fc1fbf845538
NoEyes는 소규모 신뢰 집단을 위한 Python 터미널 채팅 도구입니다. 서버는 어떤 것도 복호화하지 않으며 사용자가 누구인지 절대 보지 못합니다 - 단지 불투명한 토큰을 처리하고 암호화된 바이트를 전달할 뿐입니다.
사용자가 키를 생성해 대역 외로 공유하면, 서버는 대화 내용에 대해 아무것도 알 수 없습니다.
제3자 서버를 신뢰하지 않고 암호화 통신을 원하는 소규모 신뢰 집단, 진정한 종단 간 암호화가 적용된 비공개 채팅을 자체 호스팅하려는 사람, 또는 서버가 정확히 무엇을 보고 무엇을 볼 수 없는지 이해하려는 모든 사람에게 유용합니다.
python ui/setup.py
python ui/launch.py
`ui/launch.py`는 서버를 시작하거나 서버에 연결하는 과정을 안내합니다.
---
### 옵션 B - Python이 아직 설치되지 않은 경우
| 플랫폼 | 먼저 실행할 명령 |
|---|---|
| Linux / macOS / Termux / iSH | `sh install/install.sh` |
| Windows | `install\install.bat` |
두 스크립트 모두 Python이 없으면 설치한 후 자동으로 `setup.py`로 넘어갑니다.
---
### 옵션 C - 수동```bash
# 1. Install dependencies
pip install cryptography PyNaCl
# 2. On the server machine — generate the access key
python noeyes.py --generate-access-key
# Prints an access code hex string — share with clients via USB
# 3. On a client machine — generate chat.key from the access code
python noeyes.py --generate-chat-key <ACCESS_CODE_HEX> --key-file ./chat.key
# Distribute chat.key to all other clients via USB. Never put it on the server.
# 4. Start the server (does NOT need the key file)
python noeyes.py --server --port 5000
# Start without bore tunnel (LAN / static IP / custom tunnel)
python noeyes.py --server --port 5000 --no-bore
# Start without adding a firewall rule (not needed when using bore tunnel)
python noeyes.py --server --port 5000 --no-firewall
# 5. Connect clients - each person needs their own identity file
python noeyes.py --connect SERVER_IP --port 5000 --username alice --key-file ./chat.key --identity-path ~/.noeyes/identity_alice.key
python noeyes.py --connect SERVER_IP --port 5000 --username bob --key-file ./chat.key --identity-path ~/.noeyes/identity_bob.key
중요: 각 사용자는 고유한 신원 파일을 가져야 합니다. 두 클라이언트가 같은 신원 파일을 공유하면 동일한 받은편지함 토큰을 얻게 되며 서버는 중복 세션으로 간주하여 두 번째 클라이언트를 거부합니다. 신원 파일은 첫 실행 시 자동으로 생성되며, 사용자마다 고유한
--identity-path를 전달하기만 하면 됩니다.
Termux를 F-Droid에서 다운로드하세요(권장): https://f-droid.org/packages/com.termux/
세션을 유지하세요 - 앱을 전환할 때 NoEyes가 계속 실행되도록 tmux를 설치하세요:```bash pkg install tmux -y tmux python ui/launch.py
**저장 권한** - 이것이 없으면 파일 전송이 실패합니다:```bash
termux-setup-storage
▶로 강조 표시.각 절반은 독립적으로 스크롤됩니다. **^P**를 눌러 패널을 숨기면 전체 너비 채팅 보기가 됩니다.
모든 메시지 앞에 !tag를 붙이면 모두에게 색상으로 표시되고 알림 소리가 울립니다. 태그는 암호화된 페이로드 내부에서 전달되며 서버는 이를 볼 수 없습니다.
예시:``` !danger server is going down in 5 minutes !ok deployment successful !req can someone review my PR?
`sfx/` 폴더에서 소리가 재생됩니다. 태그 이름에 해당하는 `.wav`, `.mp3`, `.ogg`, `.aiff`, `.flac` 또는 `.m4a` 파일을 넣으세요 (예: `sfx/danger.wav`). 파일을 찾을 수 없으면 터미널 벨로 대체됩니다. 모든 소리를 비활성화하려면 `/notify off`를 사용하세요.
---
## 아키텍처
> 🗺️ **[라이브 대화형 보안 맵](https://ymsniper.github.io/NoEyes/)** — 대화형 다이어그램에서 전체 암호화 아키텍처, 위협 모델, 제로 메타데이터 라우팅을 시각적으로 분석합니다.```
┌──────────────────────────────────────────────────────────────────────┐
│ Alice ──────────────────────────────────────────── Bob │
│ │ Encrypted payload (opaque) │ │
│ │ │ │ │
│ └────────────► SERVER ─┴◄──────────────────────────┘ │
│ │ │
│ Zero-metadata blind forwarder: │
│ routes by opaque inbox tokens only │
│ { "to": "3f9a1c...", "type": "privmsg" } │
│ forwards encrypted bytes verbatim │
└──────────────────────────────────────────────────────────────────────┘
WHAT THE SERVER SEES: WHAT THE SERVER NEVER SEES:
· Encrypted bytes it can't read · Usernames or display names
· Opaque inbox tokens (blake2s) · Room names
· Opaque room tokens (blake2s) · Who is messaging whom
· Frame byte length · Message content
· Connection timing · File contents
· Ed25519 public keys
· DH key exchange values
모든 클라이언트는 연결 전에 로컬에서 두 개의 불투명 토큰을 계산합니다:``` inbox_token = blake2s(identity_vk_bytes, digest_size=16) room_token = blake2s((room_name + group_key_hex).encode(), digest_size=16)
서버는 이러한 토큰만으로 모든 프레임을 라우팅합니다. 디스플레이 이름, 방 이름 또는 공개 키를 저장하지 않습니다. 발신자 신원은 라우팅 헤더가 아닌 암호화된 페이로드(봉인된 발신자) **내부**로 전달됩니다.
### 키 유도 체인```
chat.key (shared secret)
│
├─ BLAKE2b("general") ──► room_key["general"] (isolated per room)
├─ BLAKE2b("dev") ──► room_key["dev"]
└─ BLAKE2b("ops") ──► room_key["ops"]
X25519 DH (per user pair, automatic on first /msg)
alice_ephemeral + bob_ephemeral ──► shared_secret
│
BLAKE2b
│
pairwise_key (private messages)
│
BLAKE2b(transfer_id) ──► chacha20_key (files)
password + random_salt (32 bytes, os.urandom) │ └─ BLAKE2b(password, key=salt, person="identity_v2") │ derived_key ──► encrypts Ed25519 signing key at rest
모든 신원 파일은 고유한 임의의 솔트를 얻으므로 레인보우 테이블은 쓸모없다.
---
## 보안 요약
| 계층 | 메커니즘 | 비고 |
|---|---|---|
| 순방향 비밀성(래칫) | Sender Keys — 메시지별 BLAKE2b 체인 KDF + XSalsa20-Poly1305 | 메시지별 고유 키, 놓친 메시지에 대한 빠른 전달 |
| 그룹 채팅 | XSalsa20-Poly1305 (PyNaCl secretbox) | BLAKE2b를 통한 방별 키 |
| 개인 메시지 | X25519 쌍방향 키가 포함된 XSalsa20-Poly1305 | Ed25519 서명, TOFU 검증 |
| 파일 전송 | ChaCha20-Poly1305 | BLAKE2b를 통한 전송별 키, Ed25519 서명, 재연결 시 일시 중지/재개 |
| 발신자 신원 | Sealed sender | 암호화된 페이로드 안의 사용자 이름 + 서명, 라우팅 헤더에는 절대 포함되지 않음 |
| 신원 | Ed25519 키 쌍 | 사용자별 신원 파일, BLAKE2b + 임의 솔트로 비밀번호 암호화 |
| 키 파생 | BLAKE2b (PyNaCl) | 개인화 파라미터로 도메인 분리, 레인보우 테이블 없음 |
| 서버 라우팅 | 불투명한 blake2s 토큰 | 서버는 사용자 이름, 방 이름 또는 공개 키를 절대 저장하지 않음 |
| 전송 | TLS (기본적으로 켜짐) | TOFU 인증서 고정, 지문 불일치 시 연결 중단 |
| DH 무결성 | Ed25519 서명된 DH 공개 키 | 쌍방향 키 교환 시 MITM 방지 |
| 재생 방지 | 방별 메시지 ID 덱 | 재생된 프레임은 조용히 폐기됨 |
| DoS 방지 | 연결 상한 + 조인 시간 초과 + 속도 제한 | 최대 200개 연결, 10초 조인 시간 초과 |
| 방 격리 | `BLAKE2b(master_key, room_name)` | 방마다 암호학적으로 격리됨 |
### 위협 모델
NoEyes는 **소규모 신뢰 그룹**을 위해 설계되었다. 다음에 대한 강력한 보호를 제공한다:
- 수동적 네트워크 관찰자 - 모든 트래픽은 TLS + E2E 암호화됨
- 손상된 bore.pub 릴레이 - 릴레이는 암호화된 바이트와 연결 타이밍만 볼 수 있음
- 손상된 서버 머신 - 서버는 제로 지식, RAM에 유용한 것이 없음
- 연결에 대한 MITM - TLS 인증서 고정 + Ed25519 서명된 DH 키
- 기기를 도난당한 경우 - 신원 키는 저장 시 비밀번호로 암호화됨
- 재생 공격 - MID 기반 방별 재생 보호
---
## 온라인 서버 실행 (bore pub)
집에서 NoEyes 서버를 시작하면 컴퓨터가 로컬 IP를 얻는다. 네트워크 외부의 누군가가 연결하려면 일반적으로 라우터에서 포트를 포워딩해야 하는데, CGNAT 또는 통신사 수준 차단으로 인해 실패하는 경우가 많다.
**bore pub**은 컴퓨터에서 공용 릴레이로의 보안 터널을 통해 이 문제를 해결하며, 라우터를 건드리지 않고 서버에 즉시 공용 주소를 제공한다.
**bore**는 [Eric Zhang (@ekzhang)](https://github.com/ekzhang/bore)이 만든 오픈소스 TCP 터널 도구이다. NoEyes 서버를 실행하면 자동으로 시작된다:```
bore local 5000 --to bore.pub
릴레이가 임의의 포트를 할당하고 bore.pub:12345와 같은 주소를 출력합니다. 그 주소를 그룹과 공유하세요:```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --username alice --identity-path ~/.noeyes/identity_alice.key
Everything is still end-to-end encrypted, bore only forwards raw bytes.
### bore 포트 변경 시 자동 재연결
bore.pub는 **서버가 재시작될 때마다 임의의 포트**를 할당합니다. 일반적으로 이는 매번 모든 사람과 주소를 다시 공유해야 함을 의미합니다. NoEyes는 세 가지 복구 계층으로 이를 자동 처리합니다:
**1. Migrate 이벤트 (즉시)**
bore가 포트를 다시 할당하면 서버는 새 포트 번호와 함께 서명된 `migrate` 이벤트를 연결된 모든 클라이언트에 브로드캐스트합니다. 클라이언트는 자동으로 연결을 끊고 포트를 업데이트한 후 자동으로 재연결합니다. 15초의 조용한 구간이 참가/퇴장 알림을 억제하여 채팅 화면이 깜빡이지 않습니다.
**2. 디스커버리 서비스 (migrate를 놓친 클라이언트)**
포트가 변경되었을 때 오프라인이었던 클라이언트는 재연결을 시도할 때마다 무료 익명 키-값 서비스(`keyvalue.immanuel.co`)를 폴링합니다. 서버는 bore가 재시작될 때마다 새 bore 포트를 자동으로 해당 서비스에 게시합니다. 조회 키는 그룹 키에서 파생되며 계정이나 등록이 필요 없고 완전히 익명입니다.
**3. `auth_ok`의 포트 (충돌 복구)**
클라이언트가 모든 것을 놓친 경우(서버 충돌로 migrate 브로드캐스트가 전송되지 않은 경우), 서버는 `auth_ok` 핸드셰이크 응답에 현재 bore 포트를 포함합니다. 클라이언트는 다음 연결 성공 시 스스로 수정합니다.
bore.pub 포트 변경은 사용자에게 투명하게 처리됩니다. 채팅은 수초 내에 자동으로 계속되고, 파일 전송은 중단된 지점부터 일시 중지 후 재개됩니다.
디스커버리를 비활성화하려면(에어갭 설정 또는 비공개 릴레이):```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --no-discovery
| 제한 사항 | 세부 사항 |
|---|---|
| 가동 시간 보장 없음 | bore.pub는 자원봉사 서비스라 중단될 수 있습니다 |
| 포트는 무작위 | 서버를 시작할 때마다 다른 포트가 할당되므로 주소를 다시 공유하세요 |
| 프로덕션용 아님 | 영구 설정을 위해서는 --no-bore 옵션을 사용해 VPS를 이용하세요 |
사용자가 약 10명 이상이거나, 24/7 가동이 필요하거나, 안정적인 호스트 이름이 필요하다면 저렴한 VPS(Hetzner €4/월, DigitalOcean $4/월, Oracle Cloud 무료 등급)에서 실행하세요:```bash python noeyes.py --server --port 5000 --no-bore
### 방화벽 참고 사항
bore tunnel을 사용할 때는 방화벽 규칙이 **필요하지 않습니다**. 직접 연결(LAN, 고정 IP, 수동 포트 포워딩)의 경우에만 필요합니다:```bash
python noeyes.py --server --port 5000 --no-firewall # bore tunnel, skip firewall rule
python noeyes.py --server --port 5000 --no-bore --no-firewall # VPS, manage firewall separately
python noeyes.py --generate-access-key
python noeyes.py --generate-chat-key <ACCESS_CODE_HEX> --key-file ./chat.key
python ui/launch.py # → Generate Key
cp ~/.noeyes/identity.key /backup/identity.key
cat ~/.noeyes/tofu_pubkeys.json
---
## 프로젝트 구조```
NoEyes/
├── noeyes.py Entry point and CLI argument parser
├── requirements.txt pip dependencies (just: cryptography)
│
├── core/
│ ├── encryption.py All crypto: XSalsa20-Poly1305, ChaCha20-Poly1305, X25519, Ed25519, BLAKE2b
│ ├── ratchet.py Sender Keys forward secrecy: SenderChain + RatchetState
│ ├── animation.py CRT boot and ratchet activation animations with SFX
│ ├── sounds.py Cross-platform sound playback (WAV/MP3, Linux/macOS/Windows)
│ ├── identity.py Ed25519 keypair generation and TOFU pubkey store
│ ├── utils.py Terminal output, ANSI colours, TUI chrome
│ └── config.py Configuration loading and CLI parsing
│
├── network/
│ ├── server.py Async zero-metadata blind-forwarder server
│ ├── client.py Terminal chat client (E2E, DH, TOFU, file transfer)
│ ├── client_ratchet.py RatchetMixin — /ratchet command flow, migration wait
│ ├── client_dh.py X25519 DH handshake mixin
│ ├── client_send.py Outgoing message encryption (static + ratchet paths)
│ ├── client_recv.py Incoming frame routing and decryption
│ └── client_commands.py Input loop, command dispatch, help
│
├── ui/
│ ├── launch.py Guided launcher, arrow-key menu UI
│ └── setup.py Dependency wizard, auto-installs what's needed
│
├── install/
│ ├── install.sh Bootstrap for Linux / macOS / Termux / iSH
│ ├── install.bat Bootstrap for Windows (CMD and PowerShell)
│ ├── install.py Cross-platform Python installer
│ └── uninstall.py Remove all NoEyes dependencies for clean reinstall
│
├── docs/
│ ├── README.md This file
│ └── CHANGELOG.md Version history
│
├── update.py Self-updater, pulls latest from GitHub
└── sfx/ Notification sounds
PyNaCl (XSalsa20-Poly1305, BLAKE2b) + cryptography (ChaCha20-Poly1305, X25519, Ed25519, TLS)threading (클라이언트별 recv + input + sender 스레드), 서버의 asynciotermios⚠️ 연구 및 교육용으로만 사용 - 실험적 프로젝트입니다.
| 기능 | 설명 |
|---|
| 제로 메타데이터 서버 | 서버는 사용자 이름, 룸 이름, 공개 키를 절대 볼 수 없고 오직 불투명한 토큰만 봅니다 |
| 봉인된 발신자 | 발신자 신원은 라우팅 헤더가 아닌 암호화된 페이로드 안에만 존재합니다 |
| 블라인드 포워더 서버 | 복호화 없음, 서버는 읽을 수 없는 암호화 블롭을 전달만 합니다 |
| 순방향 비밀성 | /ratchet start — Sender Keys 프로토콜, 각 메시지는 고유한 파생 키로 암호화되며 현재 키가 유출되어도 과거 메시지는 안전합니다 |
| 그룹 채팅 | 룸별 XSalsa20-Poly1305 키를 BLAKE2b로 파생, 룸은 암호학적으로 격리됩니다 |
| 개인 메시지 | 첫 접촉 시 X25519 DH 핸드셰이크, 두 당사자만 보유한 일대일 키 |
| 파일 전송 | ChaCha20-Poly1305 스트리밍, 크기 제한 없음, 낮은 RAM 사용량, 재연결 시 일시 중지/재개 |
| Ed25519 신원 | 자동 생성된 서명 키, 모든 메시지와 파일에 서명됩니다 |
| TOFU | 처음 본 키를 신뢰하고, 키 불일치 시 눈에 띄는 보안 경고가 표시됩니다 |
| 무작위 PBKDF2 솔트 | 배포마다 고유한 무작위 솔트가 생성되어 레인보우 테이블이 무용지물이 됩니다 |
| TLS + 인증서 고정 | 전송 계층이 암호화되고, TOFU로 첫 접촉 시 서버 인증서가 고정됩니다 |
| 재생 방지 | 룸별 메시지 ID 덱, 재생된 프레임은 조용히 폐기됩니다 |
| 분할 사이드바 패널 | 룸(위)과 사용자(아래)가 항상 표시되고 각 절반은 독립적으로 스크롤됩니다 |
| CRT 부팅 애니메이션 | 시작 시 사운드가 포함된 전체 화면 인광 효과 |
| 래칫 활성화 애니메이션 | 점자 기어 아트, 글리치 깜빡임, 스포트라이트 스윕, 동기화된 효과음, TUI 크롬이 빨간색으로 전환되는 전체 화면 CRT 효과 |
| 안내형 런처 | 화살표 키 메뉴 UI, 명령줄 경험 불필요 |
| 자동 의존성 설치 프로그램 | 플랫폼을 감지해 누락된 항목을 설치하며, 변경 전에 먼저 묻습니다 |
| 명령 | 설명 |
|---|
/help | 모든 명령 표시 |
/quit | 연결 끊고 종료 |
/clear | 화면에서 메시지 지우기 |
/users | 현재 방의 사용자 나열 |
/join <room> | 다른 방으로 전환 (활성 래칫 중이면 경고) |
/leave | 일반 방으로 돌아가기 (활성 래칫 중이면 경고) |
/msg <user> <text> | E2E 암호화된 비공개 메시지 보내기 |
/send <user> <file> | 암호화된 파일 보내기 |
/whoami | 자신의 신원 지문 표시 |
/trust <user> | 사용자가 재설치한 후 새 키 신뢰 |
/notify on|off | 알림 소리 켜기/끄기 전환 |
/ratchet start | 모든 방 구성원에게 순방향 비밀성 롤링 키 제안 (모두가 확인해야 함) |
/ratchet invite <u> | 사용자가 다시 합류한 후 래칫에 다시 초대 (전체 재시작 유발 — 체인 키 전달 없음) |
/proceed | 마이그레이션 대기 중 오프라인 피어 제외 투표 후 재개 |
| 키 | 동작 |
|---|
↑ / ↓ | 채팅 위/아래로 스크롤 |
PgUp / PgDn | 채팅 한 페이지 스크롤 |
^P (Ctrl+P) | 사이드바 패널 표시 / 숨기기 |
^C | 종료 |
| 태그 | 색상 | 용도 |
|---|
!ok <msg> | 🟢 초록 | 성공, 확인, 완료 |
!warn <msg> | 🟡 노랑 | 경고, 주의 |
!danger <msg> | 🔴 빨강 | 치명적, 긴급, 비상 |
!info <msg> | 🔵 파랑 | 상태 업데이트, 참고 |
!req <msg> | 🟣 보라 | 요청, 조치 필요 |
!? <msg> | 🩵 청록 | 질문, 의견 요청 |
| 플랫폼 | 사용되는 패키지 매니저 |
|---|
| Ubuntu / Debian / Mint | apt-get |
| Fedora / RHEL / CentOS | dnf / yum |
| Arch / Manjaro | pacman |
| Alpine / iSH (iOS) | apk |
| openSUSE | zypper |
| Void Linux | xbps-install |
| macOS | Homebrew (없으면 자동 설치) |
| Android (Termux) | pkg |
| Windows | winget / Chocolatey / Scoop |