Ymsniper/NoEyes

GitHub: Ymsniper/NoEyes

一个采用零元数据盲转发架构的 Python 端到端加密终端聊天工具,服务器无法解密或获知任何通信元数据。

Stars: 146 | Forks: 10

# NoEyes 🔒 安全终端聊天 [![在 OSINTech 的 OSINT Anatomy 中被推荐](https://img.shields.io/badge/Featured%20in-OSINTech%20OSINT%20Anatomy%20%23157-blue?style=for-the-badge&logo=substack&logoColor=white)](https://osintech.substack.com/p/osintechs-timeline-157-09042026) ## 实际演示 ### 完整展示 - 3 个客户端、聊天室、私信、侧边栏面板 https://github.com/user-attachments/assets/d9faabfb-73bd-46dd-92b2-23f63daf5b06 ### install.sh - 引导安装程序 https://github.com/user-attachments/assets/e8e0220d-cd7d-45a2-9443-9a5f20b57f12 ### install.py - 通用 Python 安装程序 https://github.com/user-attachments/assets/15fb383d-a02a-433e-bbd9-8ebadecf9481 ### 服务器 - 引导式启动器 https://github.com/user-attachments/assets/bca10cb1-6959-425d-96d6-fc1fbf845538 ## 什么是 NoEyes? NoEyes 是一个面向小型信任群的 Python 终端聊天工具。服务器**绝不解密任何内容**,也**绝不知道你是谁**——它只处理不透明的 token 并转发加密的字节。 你生成密钥,通过带外方式分享,服务器对你的对话一无所知。 适用于希望在无需信任任何第三方服务器的情况下进行加密通信的小型受信群体、希望搭建具有真正端到端加密的私有聊天室的人,或者任何想确切了解服务器能看到和不能看到什么的人。 ## 功能 | 功能 | 详情 | |---|---| | **零元数据服务器** | 服务器永远看不到用户名、聊天室名或公钥,只能看到不透明的 token | | **密封发件人** | 发送者身份位于加密的 payload 内部,永远不会出现在路由 header 中 | | **盲转发服务器** | 零解密,服务器只转发它无法读取的加密数据块 | | **前向保密** | `/ratchet start` — Sender Keys 协议,每条消息都使用唯一的派生密钥加密,即使当前密钥泄露,过去的消息也安全 | | **群聊** | 每个聊天室通过 BLAKE2b 派生 XSalsa20-Poly1305 密钥,聊天室之间实现加密隔离 | | **私信** | 首次接触时进行 X25519 DH 握手,仅通信双方持有配对密钥 | | **文件传输** | ChaCha20-Poly1305 流式传输,不限大小,低内存占用,支持跨重连暂停/恢复 | | **Ed25519 身份** | 自动生成签名密钥,所有消息和文件均经过签名 | | **TOFU** | 信任首次看到的密钥;密钥不匹配会触发可见的安全警告 | | **随机 PBKDF2 salt** | 每次部署都会获得唯一的随机 salt,彩虹表无效 | | **TLS + 证书绑定** | 传输加密,首次接触时通过 TOFU 绑定服务器证书 | | **重放保护** | 基于每个聊天室的消息 ID 队列,静默丢弃重放的帧 | | **分屏侧边栏面板** | 聊天室(顶部)和用户(底部)始终可见,每部分独立滚动 | | **CRT 启动动画** | 启动时带有音效的全屏荧光效果 | | **Ratchet 激活动画** | 带有盲文齿轮艺术、故障闪烁、聚光灯扫过的全屏 CRT 效果、同步 SFX,以及 TUI 边框过渡为红色 | | **引导式启动器** | 方向键菜单 UI,无需命令行经验 | | **自动依赖安装器** | 检测你的平台,安装缺失的组件,在更改任何内容之前会询问 | ## 快速开始 ### 选项 A - 引导式(推荐初学者使用) ``` # 运行 setup wizard - 自动安装 Python、pip 和 dependencies python ui/setup.py # 启动 NoEyes 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 - 手动 ``` # 安装 dependencies pip install cryptography PyNaCl # 在服务器机器上 — 生成 access key python noeyes.py --generate-access-key # 打印出 access code hex string — 通过 USB 与客户端共享 # 在客户端机器上 — 从 access code 生成 chat.key python noeyes.py --generate-chat-key --key-file ./chat.key # 通过 USB 将 chat.key 分发给所有其他客户端。切勿将其放在服务器上。 # 启动服务器(不需要 key 文件) python noeyes.py --server --port 5000 # 不使用 bore tunnel 启动(LAN / static IP / 自定义 tunnel) python noeyes.py --server --port 5000 --no-bore # 不添加 firewall rule 启动(使用 bore tunnel 时不需要) python noeyes.py --server --port 5000 --no-firewall # 连接客户端 - 每个人都需要自己的 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 ``` ## 在 Termux (Android) 上运行 从 **F-Droid** 下载 Termux(推荐):https://f-droid.org/packages/com.termux/ **保持会话存活** - 安装 tmux,以便在切换应用时 NoEyes 能继续运行: ``` pkg install tmux -y tmux python ui/launch.py # 按 Volume Down + D 进行 detach(保持在后台运行) # tmux attach 来返回 ``` **存储权限** - 没有此权限,文件传输将失败: ``` termux-setup-storage ``` ## 聊天内命令 | 命令 | 描述 | |---|---| | `/help` | 显示所有命令 | | `/quit` | 断开连接并退出 | | `/clear` | 清除屏幕上的消息 | | `/users` | 列出当前聊天室中的用户 | | `/join ` | 切换到某个聊天室(如果在活跃的 ratchet 中会发出警告) | | `/leave` | 返回到通用聊天室(如果在活跃的 ratchet 中会发出警告) | | `/msg ` | 发送 E2E 加密的私信 | | `/send ` | 发送加密文件 | | `/whoami` | 显示你的身份指纹 | | `/trust ` | 在用户重新安装后信任其新密钥 | | `/notify on\|off` | 切换通知声音 | | `/ratchet start` | 向聊天室的所有成员提议前向保密滚动密钥(所有人都必须确认) | | `/ratchet invite ` | 在用户重新加入后重新邀请其参与 ratchet(触发完全重启 — 不转发链密钥) | | `/proceed` | 在迁移等待期间,投票决定丢弃离线对等端并恢复 | ## TUI 键盘快捷键 | 按键 | 动作 | |---|---| | `↑` / `↓` | 向上 / 向下滚动聊天 | | `PgUp` / `PgDn` | 翻页滚动聊天 | | `^P` (Ctrl+P) | 显示 / 隐藏侧边栏面板 | | `^C` | 退出 | ### 侧边栏面板 - **上半部分 - 聊天室 (ROOMS)** - 本次会话中加入的所有聊天室。当前活跃的聊天室以 `▶` 高亮显示。 - **下半部分 - 用户 (USERS)** - 当前在你的活跃聊天室中的所有人。 每部分均可独立滚动。按下 **`^P`** 隐藏面板以获得全宽聊天视图。 ## 消息标签 在任何消息前加上 `!tag` 可以为所有人将其标记为彩色,并触发通知音。标签传输位于**加密的 payload 内部**,服务器永远看不到它们。 | 标签 | 颜色 | 用途 | |---|---|---| | `!ok ` | 🟢 绿色 | 成功、已确认、完成 | | `!warn ` | 🟡 黄色 | 警告、提醒 | | `!danger ` | 🔴 红色 | 关键、紧急、突发事件 | | `!info ` | 🔵 蓝色 | 状态更新、供参考 | | `!req ` | 🟣 紫色 | 请求、需要采取行动 | | `!? ` | 🩵 青色 | 提问、征求输入 | **示例:** ``` !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` 可禁用所有声音。 ## 架构 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ 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 ``` ### 零元数据路由模型 每个客户端在连接之前都会在本地计算两个不透明的 token: ``` inbox_token = blake2s(identity_vk_bytes, digest_size=16) room_token = blake2s((room_name + group_key_hex).encode(), digest_size=16) ``` 服务器仅根据这些 token 来路由所有帧。它从不存储显示名称、聊天室名称或公钥。发送者身份位于**加密的 payload 内部**(密封发件人),而不是在路由 header 中。 ### 密钥派生链 ``` 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 ``` 每个身份文件都会获得唯一的随机 salt,彩虹表无效。 ## 安全摘要 | 层级 | 机制 | 备注 | |---|---|---| | 前向保密 (ratchet) | Sender Keys — BLAKE2b 链 KDF + 每条消息 XSalsa20-Poly1305 | 每条消息具有唯一密钥,支持针对错过的消息进行快进 | | 群聊 | XSalsa20-Poly1305 (PyNaCl secretbox) | 每个聊天室的密钥通过 BLAKE2b 派生 | | 私信 | 带有 X25519 配对密钥的 XSalsa20-Poly1305 | Ed25519 签名,经 TOFU 验证 | | 文件传输 | ChaCha20-Poly1305 | 每次传输的密钥通过 BLAKE2b 派生,Ed25519 签名,支持跨重连暂停/恢复 | | 发送者身份 | 密封发件人 | 用户名 + 签名位于加密的 payload 内部,绝不出现在路由 header 中 | | 身份 | Ed25519 密钥对 | 基于用户的身份文件,使用 BLAKE2b + 随机 salt 进行密码加密 | | 密钥派生 | BLAKE2b (PyNaCl) | 通过 personalisation parameter 进行域分离,无彩虹表 | | 服务器路由 | 不透明的 blake2s token | 服务器从不存储用户名、聊天室名或公钥 | | 传输 | 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 密钥 - 盗窃你的设备的人 - 身份密钥在静止状态下经过密码加密 - 重放攻击 - 基于消息 ID (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` 的地址。与你的群组分享该地址: ``` python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --username alice --identity-path ~/.noeyes/identity_alice.key ``` 一切仍然是端到端加密的,bore 仅转发原始字节。 ### 跨 bore 端口变化的自动重连 bore.pub **每次服务器重启时都会分配一个随机端口**。通常这意味着每次都需要与所有人重新分享地址。NoEyes 通过三个恢复层自动处理此问题: **1. 迁移事件(即时)** 当 bore 重新分配端口时,服务器会向所有已连接的客户端广播一个带有新端口号的签名 `migrate` 事件。客户端会静默断开连接、更新端口并自动重新连接。15 秒的静默窗口会抑制加入/离开的提示信息,防止聊天界面闪烁。 **2. 发现服务(针对错过迁移的客户端)** 如果客户端在端口更改时处于离线状态,它会在每次重连尝试时轮询一个免费的匿名键值服务 (`keyvalue.immanuel.co`)。每次 bore 重启时,都会自动将新的 bore 端口发布到那里。查找密钥是从你的群组密钥派生的,不需要帐户或注册,完全匿名。 **3. `auth_ok` 中的端口(崩溃恢复)** 如果客户端错过了所有信息(服务器崩溃,从未发送迁移广播),服务器会在 `auth_ok` 握手响应中包含当前的 bore 端口。客户端将在下一次成功连接时进行自我纠正。 bore.pub 端口的变化对用户是透明的。聊天会在几秒钟内自动继续,文件传输会暂停并从中断处恢复。 要禁用发现服务(适用于隔离网络设置或私有中继): ``` python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --no-discovery ``` ### bore pub 的限制 | 限制 | 详情 | |---|---| | **无正常运行时间保证** | bore.pub 是一项志愿性质的服务,它可能会宕机 | | **端口是随机的** | 每次服务器启动都会获得不同的端口,需要重新分享地址 | | **不适用于生产环境** | 若需永久性设置,请使用带 `--no-bore` 的 VPS | ### 何时改用 VPS 如果用户超过约 10 人,需要 24/7 在线时间或稳定的主机名,请运行在廉价的 VPS 上(Hetzner 每月 4 欧元,DigitalOcean 每月 4 美元,Oracle Cloud 免费层): ``` python noeyes.py --server --port 5000 --no-bore ``` ### 防火墙注意事项 使用 bore 隧道时,你**不需要**防火墙规则。你仅在直接连接(LAN、静态 IP、手动端口转发)时才需要它: ``` 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 ``` ## 密钥管理 ``` # 在 SERVER 机器上 — 生成 access key (server.key) python noeyes.py --generate-access-key # 打印出 access code — 通过带外方式与客户端共享(仅限 USB) # 在 CLIENT 机器上 — 从 access code 生成 chat.key python noeyes.py --generate-chat-key --key-file ./chat.key # 通过 USB 将此 chat.key 分发给所有其他客户端 # 切勿将 chat.key 放在服务器机器上 # 或者使用 guided launcher(推荐) python ui/launch.py # → Generate Key # 备份你的 identity key cp ~/.noeyes/identity.key /backup/identity.key # 查看你当前信任的对象(TOFU store) 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 ``` ## 技术栈 - **语言:** Python 3.9+ - **加密:** `PyNaCl` (XSalsa20-Poly1305, BLAKE2b) + `cryptography` (ChaCha20-Poly1305, X25519, Ed25519, TLS) - **网络:** 带有自定义长度前缀成帧协议的原始 TCP 套接字 - **并发:** `threading`(每个客户端包含接收 + 输入 + 发送线程),服务器端使用 `asyncio` - **终端:** ANSI 转义代码,`termios` 用于原始按键输入 ## 支持的平台 | 平台 | 使用的包管理器 | |---|---| | 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 | ⚠️ 仅限研究与教育用途 - 实验性项目。
标签:Python, 前向保密, 密码学, 底层编程, 手动系统调用, 无后门, 端到端加密, 终端聊天, 自托管, 逆向工具, 零知识服务器