Як я зробив E2E encrypted файлообмін на Python — і що з цього вийшло

💡 Усі статті, обговорення, новини про Python — в одному місці. Приєднуйтесь до Python спільноти!

Привіт, DOU! Хочу розповісти про свій пет-проєкт — SecureShare. Це десктопний застосунок для прямої передачі файлів між двома комп’ютерами з наскрізним шифруванням.

Стаття не про те, який я молодець, а скоріше про технічні рішення, граблі, і чому деякі речі зроблені саме так. Буду радий почути думки і критику.

Проблема

Мені треба було відправити конфіденційний документ людині в іншому місті. Варіанти:

  • Google Drive / Dropbox — файл лежить на чужих серверах, компанія має доступ
  • Telegram — зручно, але файл зберігається на серверах, і «секретні чати» не працюють для файлів нормально
  • Email — без коментарів
  • WeTransfer — файл тимчасово зберігається на їхніх серверах

Всі ці варіанти мають спільну проблему: мій файл потрапляє на чийсь сервер, і я не контролюю, що з ним буде далі.

Я хотів просту штуку: відправити файл напряму, щоб він був зашифрований на моєму пристрої і розшифрований тільки на пристрої отримувача. Без реєстрацій, без акаунтів.

Як це працює

Архітектура досить проста:

Відправник  ←—WSS—→  Relay-сервер  ←—WSS—→  Отримувач
  1. Обидва клієнти підключаються до relay-сервера через WebSocket (TLS)
  2. Відбувається обмін ключами X25519 (Diffie-Hellman на еліптичних кривих)
  3. Обидва бачать код верифікації — як у Signal, щоб переконатися, що ніхто не втрутився
  4. Файл шифрується AES-256-GCM і передається чанками
  5. В кінці — перевірка SHA-256 хешу

Relay-сервер — це буквально труба. Він отримує зашифровані байти від відправника і передає їх отримувачу. Сервер не знає ні імені файлу, ні його вмісту, ні навіть розміру (бо метадані теж зашифровані).

Чому саме так

X25519 + AES-256-GCM

Тут я довго думав. Є XChaCha20-Poly1305, який часто рекомендують для нових проєктів — він простіший і менш чутливий до помилок з nonce. Але я зупинився на AES-256-GCM з кількох причин:

  • Широка підтримка в бібліотеці cryptography (Python)
  • Апаратне прискорення AES-NI на більшості процесорів
  • Достатньо безпечний при правильному управлінні nonce

Для захисту від повторного використання nonce зробив систему з префіксами: кожна сторона отримує свій 4-байтний префікс (визначається порівнянням публічних ключів), плюс 8-байтний лічильник. Це гарантує, що nonce ніколи не повториться.

Чесно кажучи, якби починав зараз — можливо, обрав би XChaCha20. Менше головного болю з nonce. Але AES-GCM працює, і поки що міняти не планую.

Код верифікації

Одна з речей, яка мене бісила в інших рішеннях — відсутність захисту від MITM. Якщо relay-сервер зламали, він може підмінити ключі. Тому обидва користувачі бачать короткий код (перші 8 символів SHA-256 від shared secret), і мають його порівняти. Як у Signal, коли ви скануєте QR-код контакту.

Це не ідеально — користувачі можуть ігнорувати верифікацію. Але хоча б механізм є.

WebSocket relay замість P2P

Перша версія намагалася працювати peer-to-peer через MQTT. Це був кошмар: NAT traversal, STUN/TURN, файрволи... В кінці я здався і зробив простий WebSocket relay на VPS. Так, це додає точку відмови, але:

  • Працює через будь-який NAT і файрвол
  • Не потрібна конфігурація мережі
  • Relay можна self-hosted — підняти свій за 5 хвилин через Docker
# docker-compose.yml — весь сервер
services:
  relay:
    build: .
    restart: unless-stopped
  caddy:
    image: caddy:2
    ports: ["80:80", "443:443"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile

Auto-reconnect і resume

Це фіча, яку я не планував, але яка виявилася критичною. Уявіть: передаєте файл на 3 ГБ, і на 80% Wi-Fi відвалюється на 10 секунд.

Без resume — починай спочатку. З resume — клієнт автоматично перепідключається, повторно обмінюється ключами, і продовжує з місця зупинки. Отримувач зберігає .resume маніфест з інформацією про отримані чанки.

Це було складно зробити правильно. Особливо момент з тим, що при реконнекті потрібен новий key exchange (бо з’єднання нове), але верифікацію можна пропустити (бо reconnect_token підтверджує ідентичність).

Стек

| Компонент | Технологія | |-----------|-----------| | Клієнт | Python + CustomTkinter | | Шифрування | cryptography (X25519, AES-256-GCM, HKDF) | | Relay-сервер | Python asyncio + websockets | | TLS | Caddy + Let’s Encrypt | | DNS | DuckDNS | | Збірка | PyInstaller (.exe / Linux binary) |

Чому Python? Бо це пет-проєкт, і я хотів швидко. CustomTkinter дає нормально виглядаючий GUI з мінімумом коду. Так, .exe виходить ~30 МБ (PyInstaller), і стартує не миттєво. Якби це був комерційний продукт — обрав би щось інше.

Що не подобається

Буду чесний — є речі, які мене самого дратують:

  • Один файл за сесію. Хочеш відправити 5 файлів — запакуй в архів. Це обмеження протоколу, і міняти його — це по суті переписати transfer logic
  • Немає macOS збірки. PyInstaller + CustomTkinter + macOS = страждання. Запускається з сирців, але .app не збираю
  • 5 ГБ ліміт. Це серверне обмеження щоб один користувач не забив канал. Для більшості задач вистачає, але розумію що не для всіх
  • GUI міг бути кращим. CustomTkinter — це компроміс. Виглядає нормально, але це не Electron і не нативний UI

Як виглядає

Інтерфейс підтримує три мови (українська, англійська, німецька) з живим перемиканням без перезапуску. Dark theme за замовчуванням.

Ось реальна передача файлу на 2.9 ГБ між двома пристроями:

Відправник генерує код сесії(ooxv-zhef), ділиться ним з отримувачем. Після підключення обидва бачать код верифікації (FD79-FDBB) — якщо збігається, значить ніхто не втрутився. Далі файл летить зашифрованим потоком зі швидкістю ~15 МБ/с.

Внизу вікна — лог з усіма етапами: хеш файлу, підключення до relay, обмін ключами, верифікація, передача. Повна прозорість того, що відбувається.

Що далі

Є кілька ідей, але не впевнений в пріоритетах:

  • Веб-версія (щоб не качати .exe)
  • Підтримка передачі папок
  • Покращення UX верифікації (може QR-код?)
  • macOS збірка

Якщо у вас є думки, що з цього найважливіше — буду радий почути.

Лінки

Буду вдячний за будь-який фідбек — особливо по частині криптографії і архітектури. Якщо бачите дірки в security model — кажіть, це найцінніше.

Дякую що прочитали!

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Один файл за сесію. Хочеш відправити 5 файлів — запакуй в архів.

Разве аппка не может принять файлы от юзера, и сама сделать из них zip? А на другом конце, также распаковать?

Ось і виросло покоління...
PGP успішно вирішував задачу безпечного обміну файлами через скомпроментований сервер (тоді — email, irc, anonymous ftp), зараз — всякі DropBox/OneDrive ще років 30 тому. І успішно її вирішує навіть сьогодні.

Автор намагається винайти власний olm/megolm який вже років 10 працює в Matrix та Signal. Для саморозвитку — звісно цікава та корисна вправа.
Для реального використання — в залежності від обємів краще або Matrix або ручне шифрування PGP.

Дякую за коментар.
Згоден, PGP/Matrix/Signal давно і якісно закривають цю задачу. Я не претендую на «нову криптографію» чи заміну цих рішень.
Моя ціль — інший UX-сценарій: одноразова передача файлу людині без акаунта і без попереднього обміну ключами, але з E2E та верифікацією.
Тобто це не «краще за PGP», а «простіше для не-технічного кейсу».
Якщо є конкретні ризики в поточній реалізації — буду вдячний за технічні зауваження.

Я бачу атаку класу Artem-in-the-Middle аля Привіт, Дуров.

Обидва бачать код верифікації — як у Signal, щоб переконатися, що ніхто не втрутився

Ви тупо можете запустить 2.8*10^12 потоків алгоритму Diffie-Artem-Hellmann і у вас плюс-мінус гарантовано є credentials для того щоб обдурити конкретно 8 символів виводу. Штучка інтелекту каже що одна топова відеокарта справиться за 2 хвилини.

Я вірю, що «секретні» чати Телеграм так і працюють. Відсутність вибору якої-небудь не передбаченої Артемом реалізяції алгоритму DH це і є криптографічним кидаловом.

У вас, правда, можна поставити власний сервер — але це буде великою перемогою якщо хтось крім вас буде за це платити. Ну і обіцянка «відправити файл людині, яка не знає що таке торент-клієнт» стає просто цяцянкою — торрент клієнта можна запустити хоч на семірці, файл зашифрувати краще всього через WinRar (програма російська та алгоритм-то західний), а торрент-файл передати так само як ви передасте людині назву вашого сервісу для отримання якогось файлу. Ще є Тох, там взагалі все максимально круто і підтримується максимальна кількість платформ, а сервер схожий на торрент-трекер а не на «трубу».

Якщо людині нецікаво ставати технічно грамотною, то може їй і не треба передавати ніяких секретних файлів? Технічно неграмотна людина державу не повалить, і від дуополії відмовлятися не захоче — а від кого в Інтернеті ховатися ще? — я тупо не знаю. Воно ж скачає файл, а вінда/андроїд за замовчуванням відправить файл собі на хостинг, айос як мінімум перевірить файл на наявність цпшки.

Код до цього моменту ще не дочитав, але протирічащі параграфи все ж муляють:

Сервер не знає ні імені файлу, ні його вмісту, ні навіть розміру
5 ГБ ліміт. Це серверне обмеження щоб один користувач не забив канал.

Цікаво розібратися, а що там робить Let’s encrypt?

Дякую за аналіз, валідний пойнт! 32 біти (8 hex символів) для верифікаційного
коду дійсно замало для захисту від birthday attack — при скомпрометованому relay
потрібно лише ~2^16 спроб щоб знайти колізію. Signal використовує ~200 біт,
і це правильний орієнтир.

Збільшу довжину коду в наступному релізі. Технічно це одна строчка, але дякую
що вказали — такий фідбек найцінніший.

Щодо порівняння з Telegram — різний клас проблем. У Telegram E2E навіть
не увімкнений за замовчуванням і протокол закритий. Тут весь код відкритий,
можна верифікувати.

Те що у вас в коді utf-8 замість ASCII це ок? Перший не дає гарантії що 1 символ це 1 байт (хоча якщо це не юзер-інпут то немає різниці).
І все ж таки, для чого там Let’s Encrypt?

Пан перевигадує Syncthing?

Syncthing вирішує інше завдання — це постійна синхронізація файлів між вашими пристроями. SecureShare — це одноразова передача файлу конкретній людині, з якою ви раніше нічого не налаштовували. Різниця як між Dropbox і WeTransfer — обидва передають файли, але для різних сценаріїв.

Якщо зробити можливість передавати кілька файлів чи навіть частин одного файлу за сесію, то передача буде значно швидшою (побачив ліміт у ~15Мб/сек). Зокрема потім і папки можна буде передавати.

Згоден, multi-file transfer — це найчастіший запит. 15 МБ/с — це обмеження одного WebSocket з’єднання через relay, при multi-stream можна було б розпаралелити. Поки що в планах, дякую за підтвердження пріоритету!

лучше через WebRTC такое делать. ну и навайбкодить на Rust. ну а вообще, круто)

Якщо два клієнта за NAT то не все так просто)

WebRTC розглядав на початку — проблема в тому що для файлів >1 ГБ потрібен TURN сервер як fallback, і ми повертаємося до relay. Плюс WebRTC signaling теж потребує сервер. В результаті архітектура виходить складніша, а результат той самий. Але якщо колись буде веб-версія — WebRTC точно в планах. Щодо Rust — згоден, для CLI-версії relay було б ідеально, зараз Python async тримає нормально, але при масштабуванні Rust має сенс.

а в чому проблема підняти власний TURN? coTURN ставиш, і полетіли

Проблеми немає, coTURN робочий варіант. Але це ще один сервер для користувача, який треба підтримувати. З relay — один docker compose up і готово. Плюс relay гарантує що з’єднання завжди працює, а з WebRTC+TURN є сценарії де навіть TURN не допомагає (деякі корпоративні файрволи блокують UDP повністю).

любий торент-клієнт — робиш роздачу і включаєш сідування примусово. Перекидаєш торрент файл кому треба як хоч. Після того як він скачав — зупиняєш роздачу і видаляєш торент. Усьо

Торент — робочий варіант для технарів, згоден. Але тут інша задача: відправити файл людині, яка не знає що таке торент-клієнт. Плюс торент не шифрує — трафік видно провайдеру. І .torrent файл теж треба якось передати безпечно.

Підписатись на коментарі