
Безопасный терминальный чат. 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 для небольших доверенных групп. Сервер никогда ничего не расшифровывает и никогда не видит, кто вы — он только обрабатывает непрозрачные токены и пересылает зашифрованные байты.
Вы генерируете ключ, передаёте его по стороннему каналу, и сервер не узнаёт ничего о ваших разговорах.
Полезно для небольших доверенных групп, желающих зашифрованное общение без доверия любому стороннему серверу, для самостоятельного хостинга приватного чата с настоящим сквозным шифрованием, или для тех, кто хочет точно понимать, что сервер может и не может видеть.
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/
Держите сессию активной - установите tmux, чтобы NoEyes продолжал работать при переключении приложений:```bash pkg install tmux -y tmux python ui/launch.py
**Разрешения на хранение** - передача файлов не удастся без этого:```bash
termux-setup-storage
▶.Каждая половина прокручивается независимо. Нажмите ^P, чтобы скрыть панель и видеть чат во всю ширину.
Добавьте перед любым сообщением !тег, чтобы раскрасить его для всех и включить звук уведомления. Теги передаются внутри зашифрованного содержимого, сервер их никогда не видит.
Примеры:
!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`, чтобы отключить все звуки.
---
## Архитектура
> 🗺️ **[Live Interactive Security Map](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
Каждый файл идентификации получает уникальную случайную соль — радужные таблицы бесполезны.
---
## Сводка по безопасности
| Уровень | Механизм | Примечания |
|---|---|---|
| Forward secrecy (ratchet) | Sender Keys — цепочка KDF на BLAKE2b + XSalsa20-Poly1305 на сообщение | Уникальный ключ на каждое сообщение, быстрый пропуск пропущенных сообщений |
| Групповой чат | XSalsa20-Poly1305 (PyNaCl secretbox) | Ключ на комнату через BLAKE2b |
| Личные сообщения | XSalsa20-Poly1305 с парным ключом X25519 | Подписано Ed25519, проверено TOFU |
| Передача файлов | ChaCha20-Poly1305 | Ключ на передачу через BLAKE2b, подписано Ed25519, пауза/возобновление при переподключениях |
| Идентификация отправителя | Закрытый отправитель | Имя пользователя + подпись внутри зашифрованной полезной нагрузки, никогда в заголовке маршрутизации |
| Идентификация | Пара ключей Ed25519 | Файл идентификации на пользователя, зашифрован паролем с BLAKE2b + случайная соль |
| Вывод ключей | BLAKE2b (PyNaCl) | Разделение доменов через параметр персонализации, радужные таблицы бесполезны |
| Маршрутизация сервера | Непрозрачные токены blake2s | Сервер никогда не хранит имена пользователей, названия комнат или открытые ключи |
| Транспорт | TLS (включён по умолчанию) | Привязка сертификата TOFU, несовпадение отпечатка прерывает соединение |
| Целостность DH | Открытые ключи DH, подписанные Ed25519 | Предотвращает MITM при обмене парными ключами |
| Защита от повторного воспроизведения | Дек сообщений на комнату | Повторно переданные фреймы молча отбрасываются |
| Защита от DoS | Ограничение подключений + тайм-аут присоединения + ограничение скорости | Макс. 200 подключений, тайм-аут присоединения 10 с |
| Изоляция комнат | `BLAKE2b(master_key, room_name)` | Криптографическая изоляция каждой комнаты |
### Модель угроз
NoEyes разработан для **небольших доверенных групп**. Он обеспечивает надёжную защиту от:
- Пассивных наблюдателей сети — весь трафик зашифрован TLS + E2E
- Скомпрометированного ретранслятора bore.pub — ретранслятор видит только зашифрованные байты и время подключения
- Скомпрометированной серверной машины — сервер с нулевым знанием, в ОЗУ нет ничего полезного
- MITM при подключении — привязка сертификата TLS + ключи DH, подписанные Ed25519
- Кражи устройства — ключ идентификации зашифрован паролем в покое
- Атак повторного воспроизведения — защита от повтора на основе MID в каждой комнате
---
## Запуск сервера онлайн (bore pub)
Когда вы запускаете сервер NoEyes дома, ваша машина получает локальный IP-адрес. Чтобы кто-то за пределами вашей сети мог подключиться, обычно нужно пробросить порт на вашем роутере, что часто не удается из-за CGNAT или блокировок на уровне оператора.
**bore pub** решает эту проблему с помощью защищённого туннеля от вашей машины к публичному ретранслятору, предоставляя вашему серверу мгновенный публичный адрес без касания роутера.
**bore** — это инструмент TCP-туннеля с открытым исходным кодом от [Eric Zhang (@ekzhang)](https://github.com/ekzhang/bore). Когда вы запускаете сервер NoEyes, он автоматически запускает:```
bore local 5000 --to bore.pub
The relay assigns a random port and prints an address like bore.pub:12345. Share that with your group:```bash
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` (восстановление после сбоя)**
Если клиент пропустил всё (сервер упал, широковещательное сообщение о миграции не было отправлено), сервер включает текущий порт bore в ответ рукопожатия `auth_ok`. Клиент самостоятельно корректирует порт при следующем успешном подключении.
Изменения порта bore.pub прозрачны для пользователей. Чат автоматически продолжается в течение секунд, а передача файлов приостанавливается и возобновляется с того места, где остановилась.
Чтобы отключить обнаружение (автономная установка или частный ретранслятор):```bash
python noeyes.py --connect bore.pub --port 12345 --key-file ./chat.key --no-discovery
| Ограничение | Подробности |
|---|---|
| Нет гарантии бесперебойной работы | bore.pub — это волонтерский сервис, он может быть недоступен |
| Порт назначается случайно | При каждом запуске сервера назначается новый порт, делитесь новым адресом |
| Не для продакшена | Для постоянной настройки используйте VPS с флагом |
Для более чем ~10 пользователей, круглосуточной работы или стабильного имени хоста запускайте на дешевом 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 (потоки получения, ввода и отправки на каждого клиента), asyncio на сервереtermios для ввода необработанных нажатий клавиш⚠️ Только для исследований и образовательных целей — экспериментальный проект.
| Возможность | Подробности |
|---|
| Сервер с нулевой метаинформацией | Сервер никогда не видит имена пользователей, названия комнат или открытые ключи, только непрозрачные токены |
| Закрытый отправитель | Личность отправителя находится внутри зашифрованной полезной нагрузки, никогда в заголовке маршрутизации |
| Сервер-слепой форвардер | Нулевое расшифрование, сервер пересылает зашифрованные блоки, которые не может прочитать |
| Прямая секретность | /ratchet start — протокол Sender Keys, каждое сообщение шифруется уникальным производным ключом, прошлые сообщения защищены даже при утечке текущего ключа |
| Групповой чат | Ключи для комнат на основе XSalsa20-Poly1305, полученные через BLAKE2b, комнаты криптографически изолированы |
| Личные сообщения | X25519 DH рукопожатие при первом контакте, парный ключ, который есть только у двух сторон |
| Передача файлов | Потоковая передача ChaCha20-Poly1305, любой размер, низкое использование ОЗУ, пауза/возобновление при переподключении |
| Идентификация Ed25519 | Автоматически генерируемый ключ подписи, все сообщения и файлы подписаны |
| TOFU | Доверие к ключам при первом увиденном; несовпадение ключей вызывает видимое предупреждение безопасности |
| Случайная соль PBKDF2 | Каждое развёртывание получает уникальную случайную соль, радужные таблицы бесполезны |
| TLS + закрепление сертификатов | Транспорт зашифрован, сертификат сервера закрепляется при первом контакте через TOFU |
| Защита от повторного воспроизведения | Очередь идентификаторов сообщений для каждой комнаты, повторные кадры молча отбрасываются |
| Разделённая боковая панель | Комнаты (сверху) и пользователи (снизу) всегда видны, каждая половина прокручивается независимо |
| Анимация загрузки CRT | Полноэкранный эффект фосфора со звуком при запуске |
| Анимация активации Ratchet | Полноэкранный эффект CRT с брайлевским шестерёнчатым артом, мерцанием глитча, прожектором, синхронизированным звуковым сопровождением и переходом TUI-интерфейса в красный цвет |
| Управляемый лаунчер | Меню с клавишами-стрелками, не требуется опыт командной строки |
| Автоматический установщик зависимостей | Обнаруживает вашу платформу, устанавливает недостающее, спрашивает перед любыми изменениями |
| Команда | Описание |
|---|
/help | Показать все команды |
/quit | Отключиться и выйти |
/clear | Очистить сообщения с экрана |
/users | Список пользователей в текущей комнате |
/join <room> | Переключиться в комнату (предупреждает, если есть активный ratchet) |
/leave | Вернуться в общую комнату (предупреждает, если есть активный ratchet) |
/msg <user> <text> | Отправить E2E-зашифрованное личное сообщение |
/send <user> <file> | Отправить зашифрованный файл |
/whoami | Показать ваш идентификационный отпечаток |
/trust <user> | Доверять новому ключу пользователя после переустановки |
/notify on|off | Включить/выключить звуки уведомлений |
/ratchet start | Предложить всем участникам комнаты использовать ключи с прямой секретностью (все должны подтвердить) |
/ratchet invite <u> | Повторно пригласить пользователя в ratchet после его возвращения (вызывает полный перезапуск — цепочки ключей не передаются) |
/proceed | Во время ожидания миграции проголосовать за исключение отключённого участника и продолжить |
| Клавиша | Действие |
|---|
↑ / ↓ | Прокрутка чата вверх / вниз |
PgUp / PgDn | Прокрутка чата на страницу |
^P (Ctrl+P) | Показать / скрыть боковую панель |
^C | Выйти |
| Тег | Цвет | Использование |
|---|
!ok <msg> | 🟢 Зелёный | Успех, подтверждено, выполнено |
!warn <msg> | 🟡 Жёлтый | Предупреждение, внимание |
!danger <msg> | 🔴 Красный | Критично, срочно, чрезвычайно |
!info <msg> | 🔵 Синий | Обновление статуса, к сведению |
!req <msg> | 🟣 Фиолетовый | Запрос, требуется действие |
!? <msg> | 🩵 Голубой | Вопрос, запрос информации |
--no-bore| Платформа | Используемый менеджер пакетов |
|---|
| 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 |