- Установи F-Droid
- Settings → Repositories → +
- Добавь репозиторий:
https://webdotg.github.io/kharon-fdroid/repo - Найди Kharon Messenger и установи
Эфемерный. Зашифрованный. Без слежки. На своём железе.
Kharon — минималистичный зашифрованный мессенджер для небольших команд.
Сообщения живут 2 минуты, потом исчезают навсегда.
Никаких аккаунтов, телефонных номеров, истории переписки и чужих серверов.
Назван в честь Харона — перевозчика мёртвых в греческой мифологии. Что пересекает реку — не возвращается.
Большинство мессенджеров хранят сообщения на сервере, знают кто ты, и могут быть вынуждены передать данные третьим лицам.
Kharon построен на противоположном принципе:
- Сервер — тупой relay: маршрутизирует зашифрованные блобы и больше ничего не знает
- Ты сам держишь сервер — на своём железе
- Сообщения самоуничтожаются через 2 минуты — история отсутствует по замыслу
- Нет аккаунтов — твоя личность это криптографическая пара ключей, ничего больше
- Добавить контакт = обменяться публичными ключами напрямую, офлайн или через копипаст
[Твой телефон]
1. Вводишь текст
2. Шифруешь публичным ключом получателя (X25519 + XSalsa20-Poly1305)
3. Случайный nonce 24 байта prepend к шифртексту
4. Отправляешь base64 блоб через WSS
↓ TLS 1.3 ↓
[Nginx]
5. TLS termination
6. Rate limiting, лимит соединений
7. Проверка заголовка Upgrade: websocket
8. Проксирует в Node.js
[Node.js relay]
9. Получает base64 блоб — НИКОГДА не видит plaintext
10. Маршрутизирует в WebSocket соединение получателя
11. Если получатель офлайн: хранит в RAM очереди (макс 2 мин TTL)
12. Подтверждает доставку через ACK
↓ TLS 1.3 ↓
[Телефон получателя]
13. Получает base64 блоб
14. Расшифровывает своим секретным ключом
15. Показывает текст
16. Сообщение существует только в RAM — никогда не пишется на диск
На шаге 9 сервер получает примерно следующее:
{ "type": "msg", "to": "aGVs...", "payload": "bG9yZW0...", "id": "x7k2..." }Никаких метаданных о том что сказано, никакого имени отправителя, никакого хранимого времени — только зашифрованный payload и публичный ключ получателя для маршрутизации.
| Что | Алгоритм | Почему |
|---|---|---|
| Обмен ключами | X25519 (Curve25519 DH) | Быстрый, надёжный, широко аудирован |
| Шифрование | XSalsa20-Poly1305 | Аутентифицированное шифрование — обнаруживает подмену |
| Nonce | 24 случайных байта на сообщение | 192-бит энтропии, вероятность коллизии ≈ 0 |
| Хранение ключей | Android Keystore + EncryptedSharedPreferences | Аппаратная защита, нельзя извлечь без рута |
| Хранение контактов | Room + SQLCipher | База зашифрована на диске |
| Данные | Сервер видит? |
|---|---|
| Содержимое сообщений | ❌ Никогда — только зашифрованный блоб |
| Личность отправителя | ❌ Только публичный ключ (44 символа base64) |
| Личность получателя | ❌ Только публичный ключ |
| История сообщений | ❌ TTL 2 мин, потом удаляется из RAM |
| Твоё имя | ❌ Нет регистрации |
| Твой номер телефона | ❌ Не требуется |
| Твой IP адрес | |
| Частота общения |
| Данные | Место | Зашифровано? | TTL |
|---|---|---|---|
| Твоя пара ключей | Android Keystore | ✅ Аппаратно | Навсегда |
| Имена контактов | Room/SQLCipher | ✅ AES-256 | До удаления |
| Публичные ключи контактов | Room/SQLCipher | ✅ AES-256 | До удаления |
| История сообщений | Только RAM | N/A (не хранится) | Сессия |
| Очередь сообщений на сервере | Только RAM | ✅ E2E | 2 минуты |
Интернет
│
▼
[Nginx — Слой 1]
├── Только TLS 1.2/1.3 (SSL, TLS 1.0/1.1 отключены)
├── Сильные шифры: ECDHE + AES-GCM + ChaCha20
├── HSTS (max-age=63072000) — браузер запоминает на 2 года
├── Session tickets ВЫКЛЮЧЕНЫ — защита forward secrecy
├── Rate limit: 5 новых соединений/сек с IP (burst 10)
├── Лимит соединений: 10 одновременных с IP
├── Только WebSocket: не-WS запросы получают 426
└── Security headers: X-Frame-Options, CSP, X-Content-Type-Options
│
▼
[Node.js — Слой 2]
├── Hello timeout: 10 сек (нет hello = дисконнект)
├── Лимит IP соединений: 3 одновременных с IP
├── Token bucket: 3 сообщения/сек на соединение (человек не может быстрее)
├── Валидация pubKey: ровно 44 символа base64
├── Валидация payload: base64, 0–4096 байт
├── Нельзя написать самому себе
├── Лимит офлайн-очереди: 50 сообщений на пользователя
└── Sweep TTL: один интервал чистит протухшие (не N таймеров)
│
▼
[Очередь сообщений — Слой 3]
└── Только память, TTL 2 минуты, без персистентности
[Сеть]
├── Только WSS (cleartext запрещён в network_security_config.xml)
├── Certificate pinning (sha256/ВАШ_CERT_PIN)
└── Exponential backoff reconnect (1с → 2с → 4с → ... → 30с макс)
[Хранилище]
├── Ключи: EncryptedSharedPreferences + Android Keystore (AES-256-GCM)
├── Контакты: Room + SQLCipher (AES-256)
└── Сообщения: только RAM, никогда не пишутся на диск
[ОС]
├── FLAG_SECURE: нет скриншотов, скрыт в switcher задач
├── allowBackup=false: системный бэкап отключён
├── Обнаружение root: предупреждает пользователя при наличии su
└── Минимум разрешений: только INTERNET + CAMERA
[Протокол]
├── E2E шифрование: сервер никогда не видит plaintext
├── Защита от replay-атак: дедупликация последних 100 ID сообщений
└── Nonce: 24 случайных байта на сообщение, никогда не повторяются
Честно о limitations:
- Физический захват устройства — если кто-то взял твой разблокированный телефон, он видит текущую сессию чата (она в RAM). Сообщения из прошлых сессий уже исчезли.
- Скомпрометированный оператор сервера — если ты не держишь сервер сам, оператор видит метаданные соединений (кто подключается, когда, как часто). Содержимое сообщений он читать не может.
- Рутованное устройство — Android Keystore потенциально можно обойти на рутованных устройствах. Kharon предупреждает при обнаружении root.
- Анализ трафика — наблюдатель сети видит что ты подключаешься к своему серверу, но не что ты говоришь. Размеры сообщений немного раскрывают длину текста.
- Компрометация эндпоинта — если на твоём телефоне малварь с захватом экрана, ни один мессенджер не поможет.
kharon-mess/
├── server/ # Node.js relay сервер
│ ├── index.js # WebSocket сервер, обработка сообщений
│ ├── hub.js # Менеджер соединений, TTL sweep
│ ├── nginx/
│ │ └── nginx.conf # Rate limiting, TLS, security headers
│ ├── docker-compose.yml
│ ├── Dockerfile
│ └── .env.example
│
└── android/ # Android клиент (Kotlin + Jetpack Compose)
└── app/src/main/java/com/kharon/messenger/
├── crypto/
│ └── CryptoManager.kt # X25519 + XSalsa20-Poly1305
├── network/
│ └── KharonSocket.kt # WebSocket + cert pinning + backoff
├── service/
│ └── KharonForegroundService.kt # Фоновый keepalive WebSocket
├── storage/
│ └── KharonDatabase.kt # Room + SQLCipher
├── di/
│ └── AppModule.kt # Hilt DI
├── model/
│ └── Models.kt
├── viewmodel/
│ ├── ChatViewModel.kt
│ └── ContactsViewModel.kt
└── ui/
├── theme/
│ ├── KharonTheme.kt # 5 тем: Modern, Win95, ICQ, Terminal Dark/Light
│ └── KharonThemeProvider.kt
└── screens/
├── ContactsScreen.kt
├── ChatScreen.kt
├── AddContactScreen.kt
└── SettingsScreen.kt
- Docker + docker-compose
- Домен или статический IP
- TLS сертификат
git clone https://github.com/yourname/kharon-mess
cd kharon-mess/server
cp .env.example .env
# Получаем TLS сертификат (Let's Encrypt)
certbot certonly --standalone -d yourdomain.com
mkdir certs
cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem certs/
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem certs/
docker compose up -d# /etc/letsencrypt/renewal-hooks/deploy/kharon.sh
cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /path/to/server/certs/
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /path/to/server/certs/
docker compose -f /path/to/server/docker-compose.yml restart nginx| Переменная | По умолчанию | Описание |
|---|---|---|
MSG_TTL_MS |
120000 |
Время жизни сообщения в мс (2 мин) |
SWEEP_INTERVAL_MS |
30000 |
Интервал очистки протухших сообщений |
MAX_MSG_SIZE |
4096 |
Максимальный размер payload в байтах |
MAX_QUEUE_PER_USER |
50 |
Максимум офлайн-очереди на пользователя |
MAX_CONNS_PER_IP |
3 |
Максимум одновременных соединений с IP |
MSG_RATE_LIMIT |
3 |
Сообщений в секунду на соединение |
MSG_BUCKET_MAX |
9 |
Размер token bucket (запас для burst) |
cd android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apkПосле развёртывания своего сервера обнови хэш в KharonSocket.kt:
openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| base64Замени хэш в:
.add("yourdomain.com", "sha256/ВАШ_ХЭШ_ЗДЕСЬ")Два способа:
- Копипаст ключа — Add Contact → My Key → скопировать публичный ключ → отправить коллеге любым каналом → коллега вставляет в Add Contact → Enter Key
- QR код — показываешь свой QR, коллега сканирует
Публичный ключ безопасно передавать — он публичный по замыслу. Только секретный ключ (никогда не покидает устройство) может расшифровать адресованные тебе сообщения.
| Тема | Стиль |
|---|---|
| Modern | Чистый тёмный интерфейс |
| Windows 95 | Классический ретро UI с рельефными кнопками |
| ICQ | Та самая аська, зелёный цветочек |
| Terminal Dark | Зелёный на чёрном, стиль Matrix/MikroTik |
| Terminal Light | Тёмно-зелёный на белом, стиль DEC терминала |
- Групповые чаты
- Push-to-talk (режим рации)
- iOS клиент
- Удаление сообщения до истечения TTL
- Закреплённые сообщения (макс 24ч)
- Подключение через VPN туннель
ws— WebSocket серверdotenv— конфигурация окружения- Node.js 20+, Docker
lazysodium-android— обёртка libsodium (X25519, XSalsa20)okhttp— WebSocket клиент с certificate pinningroom+sqlcipher— зашифрованная локальная БДsecurity-crypto— обёртка Android Keystorezxing-android-embedded— сканер QR кодовhilt— dependency injection- Jetpack Compose — UI
MIT — делай что хочешь, поднимай свой инстанс, форкай, присылай PR.
Брал что давали, перевозил на ту сторону — и на этом всё. Никаких записей. Без возврата.
Именно так работает эта программа.
Хочешь улучшить Kharon? Вот что можно сделать:
- Push-уведомления — сейчас сообщения приходят только пока приложение открыто. Нужен FCM или UnifiedPush
- iOS клиент — Swift + SwiftUI, та же криптография через libsodium
- Forward Secrecy — Double Ratchet алгоритм как в Signal для перфектной прямой секретности
- Групповые чаты — broadcast шифрование для N участников
- Голосовые сообщения — запись + шифрование аудио
- Подтверждение доставки — двойная галочка когда получатель онлайн
- Удаление сообщений — до истечения TTL
- Закреплённые сообщения — максимум 24 часа
- Федерация серверов — несколько серверов, сообщения маршрутизируются между ними
- Tor/I2P поддержка — скрытый сервис для полной анонимности
- Reproducible builds — воспроизводимая сборка для F-Droid
- GitHub Actions CI — автоматическая сборка и публикация APK
- Тёмный режим системы — автоматическое переключение темы
- Экспорт/импорт контактов — QR-код со всеми контактами
- Виджет — статус подключения на рабочем столе
Если хочешь реализовать что-то из этого — fork + PR, всё приветствуется.
!!! и так скромненько почти мелким шрифтом после всего главного
- Protocol: Reception Hours — Клиент больше не обязан висеть в сети 24/7.
- Server: Signal
queue_end— Сервер научился понимать режимы (PULSE/LIVE) и давать команду на разрыв соединения, когда все офлайн-сообщения доставлены. - Architecture: Zero-Footprint — Теперь мессенджер может работать сессиями. Зашел -> Забрал -> Ушел.
- E2EE: Шифрование осталось прежним. Ключи только у юзеров.
- Traffic: Весь трафик идет через WebSocket с certificate pinning.
- Rate Limits: Защита от спама на сервере (Token Bucket) работает как и раньше.
- Клиент ставит "Сеанс: 60 мин".
- Сервис Android закрывается.
- Через 60 минут система будит сервис.
- Сервис за 2 секунды забирает все сообщения с сервера.
- Сервер говорит "Очередь пуста".
- Клиент засыпает еще на час.
