22-06-2026+12-47

This commit is contained in:
Artur 2026-06-22 12:47:48 +05:00
parent 5044cb6020
commit 6b7fd0531f
9 changed files with 292 additions and 115 deletions

Binary file not shown.

View File

@ -46,6 +46,9 @@ class AuthProvider extends ChangeNotifier {
String? _about; String? _about;
String? get about => _about; String? get about => _about;
String? _pubKey;
String? get pubKey => _pubKey;
String? _avatarPath; String? _avatarPath;
String? get avatarPath => _avatarPath; String? get avatarPath => _avatarPath;
@ -500,6 +503,7 @@ class AuthProvider extends ChangeNotifier {
'encrypted_private_key': keys['encrypted_private_key'], 'encrypted_private_key': keys['encrypted_private_key'],
}), }),
); );
_pubKey = keys['public_key'];
if (response.statusCode == 200) { if (response.statusCode == 200) {
final String currentUsername = _username ?? ''; final String currentUsername = _username ?? '';
@ -548,6 +552,7 @@ class AuthProvider extends ChangeNotifier {
_phone = data['phone']?.toString(); _phone = data['phone']?.toString();
_email = data['email']?.toString(); _email = data['email']?.toString();
_about = data['about']?.toString(); _about = data['about']?.toString();
_pubKey = data['public_key']?.toString();
final avatarFileId = data['avatar_file_id']?.toString(); final avatarFileId = data['avatar_file_id']?.toString();
_avatarUrl = avatarFileId != null _avatarUrl = avatarFileId != null
? '${AppConstants.baseUrl}/media/$avatarFileId' ? '${AppConstants.baseUrl}/media/$avatarFileId'

View File

@ -22,6 +22,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:archive/archive.dart';
import '/data/datasources/ws_client.dart'; import '/data/datasources/ws_client.dart';
import '/data/models/contact_model.dart'; import '/data/models/contact_model.dart';
import '/data/models/message_model.dart'; import '/data/models/message_model.dart';
@ -52,6 +53,8 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
int _apkFileSizeBytes = 0; int _apkFileSizeBytes = 0;
CancelToken? _cancelToken = CancelToken(); CancelToken? _cancelToken = CancelToken();
String? _latestApkUrl; String? _latestApkUrl;
String? _latestApkZipUrl;
String? _downloadStatus;
bool _showUpdateBanner = false; bool _showUpdateBanner = false;
bool _contactsLoaded = false; bool _contactsLoaded = false;
Timer? _contactLoadTimer; Timer? _contactLoadTimer;
@ -1146,7 +1149,7 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
'${_formatBytes(_downloadedBytes)} из ${_formatBytes(_downloadTotalBytes)}', _downloadStatus ?? '${_formatBytes(_downloadedBytes)} из ${_formatBytes(_downloadTotalBytes)}',
style: TextStyle(color: Colors.white, fontSize: 14), style: TextStyle(color: Colors.white, fontSize: 14),
), ),
), ),
@ -1575,6 +1578,7 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
setState(() { setState(() {
_showUpdateBanner = true; _showUpdateBanner = true;
_latestApkUrl = data['download_url'] ?? data['apk_url']; _latestApkUrl = data['download_url'] ?? data['apk_url'];
_latestApkZipUrl = data['apk_zip_url'];
}); });
if (_latestApkUrl != null) { if (_latestApkUrl != null) {
final size = await _fetchApkSize(_latestApkUrl!); final size = await _fetchApkSize(_latestApkUrl!);
@ -2203,24 +2207,37 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
return; return;
} }
// Логика для Android (Остается без изменений) // Логика для Android
setState(() => _isDownloading = true); setState(() {
_isDownloading = true;
_downloadStatus = null;
});
Directory? dir = await getExternalStorageDirectory(); Directory? dir = await getExternalStorageDirectory();
final path = '${dir!.path}/update.apk'; final zipPath = '${dir!.path}/update.zip';
final file = File(path); final apkPath = '${dir.path}/update.apk';
final zipFile = File(zipPath);
final apkFile = File(apkPath);
if (await file.exists()) await file.delete(); if (await zipFile.exists()) await zipFile.delete();
if (await apkFile.exists()) await apkFile.delete();
bool isZipDownloaded = false;
try {
// 1. Пробуем сначала скачать ZIP-архив
if (_latestApkZipUrl != null) {
try { try {
setState(() { setState(() {
_downloadProgress = 0.0; _downloadProgress = 0.0;
_downloadedBytes = 0; _downloadedBytes = 0;
_downloadTotalBytes = 0; _downloadTotalBytes = 0;
_downloadStatus = null;
}); });
print("Попытка скачать ZIP обновление с $_latestApkZipUrl");
await Dio().download( await Dio().download(
_latestApkUrl!, _latestApkZipUrl!,
path, zipPath,
cancelToken: _cancelToken, cancelToken: _cancelToken,
onReceiveProgress: (rec, total) { onReceiveProgress: (rec, total) {
if (mounted) { if (mounted) {
@ -2232,13 +2249,98 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
} }
}, },
); );
await OpenFilex.open(path); isZipDownloaded = true;
print("ZIP успешно скачан");
} catch (e) {
print("Не удалось скачать ZIP-архив: $e. Переключаемся на обычный APK...");
if (await zipFile.exists()) {
try {
await zipFile.delete();
} catch (_) {}
}
}
}
// 2. Если ZIP не скачался (или ссылка отсутствовала), качаем обычный APK
if (!isZipDownloaded && _latestApkUrl != null) {
try {
setState(() {
_downloadProgress = 0.0;
_downloadedBytes = 0;
_downloadTotalBytes = 0;
_downloadStatus = null;
});
print("Скачивание обычного APK с $_latestApkUrl");
await Dio().download(
_latestApkUrl!,
apkPath,
cancelToken: _cancelToken,
onReceiveProgress: (rec, total) {
if (mounted) {
setState(() {
_downloadedBytes = rec;
_downloadTotalBytes = total > 0 ? total : 0;
_downloadProgress = total > 0 ? rec / total : 0.0;
});
}
},
);
} catch (e) {
print("Ошибка при скачивании APK: $e");
rethrow;
}
}
// 3. Если скачан ZIP, распаковываем его
if (isZipDownloaded) {
try {
if (mounted) {
setState(() {
_downloadStatus = "Распаковка...";
_downloadProgress = 0.0;
});
}
print("Начало распаковки ZIP...");
final bytes = await zipFile.readAsBytes();
final archive = ZipDecoder().decodeBytes(bytes);
for (final file in archive) {
final filename = file.name;
if (file.isFile && filename.endsWith('.apk')) {
final data = file.content as List<int>;
await apkFile.writeAsBytes(data);
print("Распакован файл: $filename");
break;
}
}
// Удаляем временный zip-архив
await zipFile.delete();
} catch (e) {
print("Ошибка при распаковке архива: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ошибка при распаковке обновления')),
);
}
return;
}
}
// 4. Установка APK
if (await apkFile.exists()) {
await OpenFilex.open(apkPath);
} else {
print("Файл установки APK не найден");
}
} catch (_) { } catch (_) {
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {
_isDownloading = false; _isDownloading = false;
_downloadProgress = 0.0; _downloadProgress = 0.0;
_downloadStatus = null;
}); });
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'dart:io'; import 'dart:io';
@ -24,7 +25,6 @@ class MyProfileScreen extends StatefulWidget {
class _MyProfileScreenState extends State<MyProfileScreen> { class _MyProfileScreenState extends State<MyProfileScreen> {
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
bool _isAvatarExpanded = false; bool _isAvatarExpanded = false;
String? privKey;
double? _stableBaseHeight; double? _stableBaseHeight;
bool _avatarInteracted = false; bool _avatarInteracted = false;
@ -47,15 +47,6 @@ class _MyProfileScreenState extends State<MyProfileScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadPrivKey();
}
Future<void> _loadPrivKey() async {
final storage = const FlutterSecureStorage();
privKey = await storage.read(key: 'private_key');
if (mounted) {
setState(() {});
}
} }
@override @override
@ -301,7 +292,7 @@ class _MyProfileScreenState extends State<MyProfileScreen> {
_buildInfoRow( _buildInfoRow(
context, context,
Icons.key_rounded, Icons.key_rounded,
privKey ?? 'Отсутствует', authProv.pubKey ?? 'Отсутствует',
'Публичный E2EE ключ', 'Публичный E2EE ключ',
false, false,
), ),
@ -366,6 +357,17 @@ class _MyProfileScreenState extends State<MyProfileScreen> {
horizontal: 20, horizontal: 20,
vertical: 4, vertical: 4,
), ),
onTap: value?.isNotEmpty == true
? () {
Clipboard.setData(ClipboardData(text: value!));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$label скопирован в буфер обмена'),
duration: const Duration(seconds: 2),
),
);
}
: null,
leading: Container( leading: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -377,6 +379,8 @@ class _MyProfileScreenState extends State<MyProfileScreen> {
title: Text( title: Text(
value?.isNotEmpty == true ? value! : 'Не указано', value?.isNotEmpty == true ? value! : 'Не указано',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
subtitle: Text( subtitle: Text(
label, label,

View File

@ -26,13 +26,13 @@ packages:
source: hosted source: hosted
version: "7.7.1" version: "7.7.1"
archive: archive:
dependency: transitive dependency: "direct main"
description: description:
name: archive name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.9" version: "3.6.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -916,10 +916,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.3.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1392,14 +1392,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.2" version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -68,6 +68,7 @@ dependencies:
video_player: ^2.11.1 video_player: ^2.11.1
video_player_win: ^3.2.2 video_player_win: ^3.2.2
image_picker: ^1.2.2 image_picker: ^1.2.2
archive: ^3.6.1
permission_handler: ^12.0.1 permission_handler: ^12.0.1
wechat_assets_picker: ^9.0.0 wechat_assets_picker: ^9.0.0
photo_manager: ^3.0.0 photo_manager: ^3.0.0

View File

@ -114,6 +114,16 @@ async def get_update(platform: str = Query("android")):
) )
# Ветка для Android # Ветка для Android
if "zip" in platform.lower():
file_path = os.path.join(RELEASE_DIR, "app-release.zip")
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Файл ZIP не найден")
return FileResponse(
path=file_path,
filename="chepuhagram-release.zip",
media_type="application/zip"
)
file_path = os.path.join(RELEASE_DIR, "app-release.apk") file_path = os.path.join(RELEASE_DIR, "app-release.apk")
if not os.path.exists(file_path): if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Файл APK не найден") raise HTTPException(status_code=404, detail="Файл APK не найден")

View File

@ -465,7 +465,7 @@
} }
.code-container::before { .code-container::before {
content: 'Dart / PointyCastle / Cryptography'; content: 'Криптографический Стек';
position: absolute; position: absolute;
top: 8px; top: 8px;
right: 16px; right: 16px;
@ -544,10 +544,10 @@
<!-- Header Navigation --> <!-- Header Navigation -->
<header> <header>
<div class="container header-content"> <div class="container header-content">
<a href="../index.html" class="logo" id="headerLogo"> <a href="../" class="logo" id="headerLogo">
<span class="logo-dot"></span>Чепухаграм <span class="logo-dot"></span>Чепухаграм
</a> </a>
<a href="../index.html" class="btn-back" id="backLink"> <a href="../" class="btn-back" id="backLink">
На главную На главную
</a> </a>
</div> </div>
@ -556,9 +556,9 @@
<!-- Hero Header --> <!-- Hero Header -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<h1 id="titleHeader">Безопасность и сквозное шифрование (E2EE)</h1> <h1 id="titleHeader">Безопасность, Сквозное Шифрование и Архитектура Системы</h1>
<div class="hero-meta" id="metaDate">Обновлено: Июнь 2026 • Документация Чепухаграм</div> <div class="hero-meta" id="metaDate">Обновлено: Июнь 2026 • Документация Чепухаграм</div>
<p class="intro-text" id="introDesc">В мессенджере Чепухаграм конфиденциальность переписки обеспечивается математическими законами. Никакие третьи лица, включая разработчиков и администраторов серверов, не могут получить доступ к содержимому ваших чатов.</p> <p class="intro-text" id="introDesc">В мессенджере Чепухаграм конфиденциальность переписки обеспечивается строгими математическими законами. Никакие третьи лица, включая разработчика и администратора сервера, не могут получить доступ к содержимому ваших чатов. Данный документ подробно описывает криптографический стек, жизненный цикл сообщений и механизмы синхронизации данных.</p>
</div> </div>
</section> </section>
@ -569,43 +569,50 @@
<div class="spec-grid" id="specGrid"> <div class="spec-grid" id="specGrid">
<div class="spec-card"> <div class="spec-card">
<span class="icon">🔑</span> <span class="icon">🔑</span>
<h3>X25519</h3> <h3>X25519 (ECDH)</h3>
<p>Протокол Диффи-Хеллмана для генерации общего ключа (ECDH)</p> <p>Согласование общего ключа на базе эллиптических кривых (Curve25519) без передачи секрета по сети.</p>
</div> </div>
<div class="spec-card"> <div class="spec-card">
<span class="icon">⚙️</span> <span class="icon">⚙️</span>
<h3>AES-256-GCM</h3> <h3>AES-256-GCM</h3>
<p>Симметричное шифрование с проверкой целостности данных</p> <p>Симметричное шифрование блоков данных с проверкой целостности и подлинности (AEAD).</p>
</div> </div>
<div class="spec-card"> <div class="spec-card">
<span class="icon">🔒</span> <span class="icon">🔒</span>
<h3>PBKDF2</h3> <h3>PBKDF2-HMAC-SHA256</h3>
<p>Криптографическая деривация ключей на базе мастер-пароля (600,000 ит.)</p> <p>Криптографическая деривация ключей из мастер-пароля с использованием 600,000 итераций.</p>
</div>
<div class="spec-card">
<span class="icon">📡</span>
<h3>WebSockets & Drift DB</h3>
<p>Реалтайм-доставка сообщений и локальное структурированное хранение истории в зашифрованном виде.</p>
</div> </div>
</div> </div>
<h2 id="e2eeHeader">Принцип сквозного шифрования (E2EE)</h2> <h2 id="e2eeHeader">1. Сквозное шифрование (End-to-End Encryption)</h2>
<p>Шифрование «end-to-end» означает, что шифрование информации происходит непосредственно на устройстве отправителя, а дешифрование — только на устройстве получателя. Серверная часть выполняет лишь функцию почтальона — пересылает зашифрованные пакеты байт, не имея ключей для их расшифровки.</p> <p>С сквозным шифрованием (E2EE) ваши сообщения кодируются непосредственно перед отправкой на вашем устройстве и могут быть декодированы только на устройстве получателя. Серверная часть мессенджера Чепухаграм полностью изолирована от ключевой информации. Сервер функционирует исключительно как маршрутизатор зашифрованных бинарных пакетов и не способен восстановить исходный текст.</p>
<!-- ECDH Exchange Visual Diagram --> <!-- ECDH Exchange Visual Diagram -->
<h3>Схема согласования ключей (X25519)</h3> <h3>Схема согласования ключей (X25519 / ECDH)</h3>
<p>Для создания защищенного канала используется протокол Диффи-Хеллмана на эллиптических кривых. Ниже представлена схема независимого вычисления общего секрета:</p>
<div class="dh-diagram"> <div class="dh-diagram">
<div class="dh-entity"> <div class="dh-entity">
<div class="dh-avatar">Вы</div> <div class="dh-avatar">Вы</div>
<div class="dh-key private">Приватный ключ A</div> <div class="dh-key private">Ваш приватный ключ A (dA)</div>
<div class="dh-key public">Публичный ключ A</div> <div class="dh-key public">Ваш публичный ключ A (QA = dA * G)</div>
<div class="dh-key shared">Общий секрет (K)</div> <div class="dh-key shared">Общий секрет (K = dA * QB)</div>
</div> </div>
<div class="dh-channel"> <div class="dh-channel">
<div class="dh-arrow">публичный ключ A →</div> <div class="dh-arrow">публичный ключ A →</div>
<div class="dh-arrow">← публичный ключ B</div> <div class="dh-arrow">← публичный ключ B</div>
<p style="margin: 8px 0 0 0; text-align: center; font-size: 11px;">(сервер пересылает только публичные ключи)</p> <p style="margin: 8px 0 0 0; text-align: center; font-size: 11px;">(сервер транслирует только открытые ключи)</p>
</div> </div>
<div class="dh-entity"> <div class="dh-entity">
<div class="dh-avatar bob">Собеседник</div> <div class="dh-avatar bob">Собеседник</div>
<div class="dh-key private">Приватный ключ B</div> <div class="dh-key private">Приватный ключ B (dB)</div>
<div class="dh-key public">Публичный ключ B</div> <div class="dh-key public">Публичный ключ B (QB = dB * G)</div>
<div class="dh-key shared">Общий секрет (K)</div> <div class="dh-key shared">Общий секрет (K = dB * QA)</div>
</div> </div>
</div> </div>
@ -613,104 +620,160 @@
<div class="step-item"> <div class="step-item">
<div class="step-num">1</div> <div class="step-num">1</div>
<div class="step-content"> <div class="step-content">
<h3>Создание пары ключей</h3> <h3>Генерация локальной пары ключей</h3>
<p>При первом запуске приложения Чепухаграм генерирует на устройстве пару асимметричных ключей X25519 (приватный и публичный). Публичный ключ отправляется на сервер для того, чтобы другие пользователи могли начать с вами чат.</p> <p>При первом входе или настройке аккаунта приложение генерирует на устройстве пару асимметричных ключей X25519. Приватный ключ надёжно сохраняется во внутреннем изолированном хранилище ОС (Secure Storage / KeyStore / Keychain), а публичный отправляется в базу данных сервера для обеспечения доступности вашего контакта другим пользователям.</p>
</div> </div>
</div> </div>
<div class="step-item"> <div class="step-item">
<div class="step-num">2</div> <div class="step-num">2</div>
<div class="step-content"> <div class="step-content">
<h3>Деривация общего секрета (Shared Secret)</h3> <h3>Вычисление общего секрета (Shared Secret)</h3>
<p>Когда вы открываете чат, приложение берет ваш приватный ключ X25519 и публичный ключ собеседника, вычисляя общий секрет по алгоритму ECDH. Этот секретный ключ никогда не передается по сети — обе стороны вычисляют его независимо на своих девайсах.</p> <p>Когда вы открываете чат с пользователем, клиентское приложение автоматически запрашивает публичный ключ собеседника с сервера. Используя его совместно со своим приватным ключом, алгоритм ECDH вычисляет уникальный симметричный ключ (общий секрет). Он никогда не передаётся по сети — обе стороны получают абсолютно одинаковый ключ математическим путём.</p>
</div> </div>
</div> </div>
<div class="step-item"> <div class="step-item">
<div class="step-num">3</div> <div class="step-num">3</div>
<div class="step-content"> <div class="step-content">
<h3>Симметричное шифрование</h3> <h3>Симметричное шифрование AES-256-GCM</h3>
<p>Все отправляемые текстовые сообщения шифруются по стандарту AES-256-GCM с уникальным вектором инициализации (Nonce). Полученный шифротекст передается на сервер.</p> <p>Полученный общий секрет передается в алгоритм шифрования AES-256 в режиме Galois/Counter Mode. Каждое сообщение шифруется с использованием уникального вектора инициализации (Nonce), что делает невозможным проведение атак на основе повторяющихся шифротекстов.</p>
</div> </div>
</div> </div>
</div> </div>
<h2 id="messagesCryptoHeader">Шифрование текстовых сообщений</h2> <h2 id="messagesCryptoHeader">2. Шифрование и деривация текстовых сообщений</h2>
<p>Каждое текстовое сообщение кодируется в массив байтов, после чего для него генерируется случайный 12-байтовый вектор инициализации (nonce). Данные шифруются на ключе sharedSecret по алгоритму AES-256-GCM. Результат представляет собой склеенный массив байтов: <code>nonce (12 байт) + mac (16 байт) + ciphertext (зашифрованный текст)</code>, закодированный в Base64.</p> <p>Каждое текстовое сообщение шифруется с генерацией случайного 12-байтового вектора инициализации (IV / Nonce). Режим GCM гарантирует аутентифицированное шифрование (AEAD): к шифротексту добавляется 16-байтовый тег аутентификации (MAC), подтверждающий, что данные не были изменены при транзите через сервер.</p>
<p>Итоговая структура пакета сообщения на сервере выглядит следующим образом:</p>
<div class="block-result" style="margin-bottom: 24px; text-align: center;">
[Nonce (12 байт)] + [MAC Тег (16 байт)] + [Шифротекст (N байт)] => Кодирование в Base64
</div>
<p>Пример реализации алгоритмов шифрования и расшифрования в приложении на Dart с использованием криптографических библиотек:</p>
<div class="code-container" id="messageCode"> <div class="code-container" id="messageCode">
<pre><code>// Схематичный пример дешифровки сообщения на клиенте <pre><code>// Шифрование строки текста на общем ключе
Future&lt;String&gt; encryptMessage(String plainText, SecretKey sharedKey) async {
final algorithm = AesGcm.with256bits();
final clearTextBytes = utf8.encode(plainText);
// Генерация случайного вектора инициализации (Nonce)
final secretBox = await algorithm.encrypt(
clearTextBytes,
secretKey: sharedKey,
);
// Объединяем nonce, mac-тег и зашифрованные байты в один пакет
final combinedBytes = BytesBuilder()
..add(secretBox.nonce)
..add(secretBox.mac.bytes)
..add(secretBox.cipherText);
return base64Encode(combinedBytes.toBytes());
}
// Дешифрование сообщения на клиенте
Future&lt;String&gt; decryptMessage(String base64Data, SecretKey sharedKey) async { Future&lt;String&gt; decryptMessage(String base64Data, SecretKey sharedKey) async {
final data = base64Decode(base64Data); final rawData = base64Decode(base64Data);
if (rawData.length < 28) throw Exception("Пакет данных слишком мал");
final nonce = data.sublist(0, 12); final nonce = rawData.sublist(0, 12);
final mac = data.sublist(12, 28); final mac = rawData.sublist(12, 28);
final cipherText = data.sublist(28); final cipherText = rawData.sublist(28);
final decrypted = await aesGcm.decrypt( final algorithm = AesGcm.with256bits();
final decryptedBytes = await algorithm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)), SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
secretKey: sharedKey, secretKey: sharedKey,
); );
return utf8.decode(decrypted); return utf8.decode(decryptedBytes);
}</code></pre> }</code></pre>
</div> </div>
<h2 id="filesCryptoHeader">Поблочное шифрование медиафайлов</h2> <h2 id="filesCryptoHeader">3. Поблочное шифрование медиафайлов (Крипто-стриминг)</h2>
<p>Для предотвращения утечек данных при отправке медиафайлов (картинок, видео, голосовых заметок и документов), в Чепухаграм внедрена продвинутая поблочная система шифрования:</p> <p>Шифрование больших файлов целиком в оперативной памяти мобильных устройств приводит к её переполнению и сбоям. Кроме того, это лишает возможности начать воспроизведение видео/аудио до окончания полной загрузки файла. В мессенджере Чепухаграм внедрена технология <strong>поблочного крипто-стриминга</strong>:</p>
<ol> <ol>
<li>При выборе файла генерируется случайный симметричный ключ файла (<code>fileKey</code>).</li> <li><strong>Генерация ключа файла:</strong> Для каждого медиафайла генерируется индивидуальный случайный 256-битный ключ (<code>fileKey</code>). Это позволяет безопасно делиться ключом конкретного файла, не компрометируя общий секрет чата.</li>
<li>Ключ <code>fileKey</code> шифруется на общем ключе чата (<code>sharedSecret</code>) и отправляется на сервер как поле <code>encrypted_key</code>.</li> <li><strong>Шифрование ключа:</strong> Ключ <code>fileKey</code> зашифровывается с помощью AES-GCM на основном общем ключе чата (<code>sharedSecret</code>). Этот зашифрованный ключ прикрепляется к сообщению как поле <code>encrypted_key</code>.</li>
<li>Сам файл считывается потоком и шифруется блоками по 64 КБ с использованием <code>fileKey</code>. К каждому зашифрованному блоку добавляется 4-байтовый заголовок, содержащий точную длину блока, и уникальный вектор инициализации с тегом аутентификации MAC.</li> <li><strong>Поблочное чтение и шифрование:</strong> Исходный файл считывается потоком частями по 64 КБ. Каждый блок шифруется алгоритмом AES-256-GCM независимо.</li>
<li><strong>Форматирование блоков:</strong> К зашифрованным блокам добавляется структура:
<div class="block-result" style="margin: 12px 0; text-align: center;">
[Длина блока (4 байта)] + [Nonce (12 байт)] + [Зашифрованные данные (64 КБ)] + [Тег аутентификации MAC (16 байт)]
</div>
</li>
<li><strong>Передача на Google Drive:</strong> Зашифрованный поток отправляется напрямую в облачное хранилище через сервер. Сервер имеет доступ только к зашифрованному потоку байтов и не знает ключ <code>fileKey</code>.</li>
</ol> </ol>
<!-- Media block stream diagram --> <!-- Media block stream diagram -->
<h3>Архитектура поблочного крипто-стрима</h3>
<div class="block-stream-diagram"> <div class="block-stream-diagram">
<div class="stream-file">Исходный файл медиа</div> <div class="stream-file">Исходный медиафайл (Изображение, Видео, Голосовая заметка)</div>
<div class="stream-split">👇 Считывание блоками по 64 КБ</div> <div class="stream-split">👇 Разделение на чанки в потоке по 64 КБ</div>
<div class="stream-blocks"> <div class="stream-blocks">
<div class="stream-block"> <div class="stream-block">
<span class="block-title">Блок 1 (64 КБ)</span> <span class="block-title">Блок 1 (64 КБ)</span>
<div class="block-crypto">🔒 AES-256-GCM (fileKey)</div> <div class="block-crypto">🔒 AES-256-GCM (на ключе fileKey)</div>
<div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div> <div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div>
</div> </div>
<div class="stream-block"> <div class="stream-block">
<span class="block-title">Блок 2 (64 КБ)</span> <span class="block-title">Блок 2 (64 КБ)</span>
<div class="block-crypto">🔒 AES-256-GCM (fileKey)</div> <div class="block-crypto">🔒 AES-256-GCM (на ключе fileKey)</div>
<div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div> <div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div>
</div> </div>
<div class="stream-block tail"> <div class="stream-block tail">
<span class="block-title">Остаток (&lt;64 КБ)</span> <span class="block-title">Хвостовой Блок (&lt; 64 КБ)</span>
<div class="block-crypto">🔒 AES-256-GCM (fileKey)</div> <div class="block-crypto">🔒 AES-256-GCM (на ключе fileKey)</div>
<div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div> <div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div>
</div> </div>
</div> </div>
</div> </div>
<div class="callout" id="fileSecurityNote"> <div class="callout" id="fileSecurityNote">
<p><strong>Важно:</strong> Такой подход позволяет осуществлять потоковую дешифрацию медиа во время загрузки (стриминг), благодаря чему видео и аудиозаписи начинают воспроизводиться еще до полной загрузки файла.</p> <p><strong>Преимущество стриминга:</strong> При скачивании получатель расшифровывает блоки на лету по мере их поступления по сети. Плеер приложения начинает воспроизведение видео- или аудиофайла мгновенно, не дожидаясь полной загрузки сотен мегабайт с Google Drive.</p>
</div> </div>
<h2 id="masterPassHeader">Резервная копия ключей и мастер-пароль</h2> <h2 id="masterPassHeader">4. Защита ключей и облачное резервное копирование (Backup)</h2>
<p>Поскольку приватный ключ X25519 хранится в изолированном защищенном хранилище (Secure Storage) вашего смартфона, при переустановке приложения вы можете потерять доступ к переписке. Для предотвращения этого в Чепухаграм создана система защищенных резервных копий:</p> <p>Потеря устройства или очистка приложения без резервной копии привела бы к безвозвратной потере всей переписки, так как приватный ключ X25519 существует в единственном экземпляре на смартфоне. Для решения этой проблемы Чепухаграм реализует безопасную схему резервного копирования ключей:</p>
<ul> <ul>
<li>Вы придумываете сложный <strong>Мастер-пароль</strong>.</li> <li><strong>Мастер-пароль:</strong> Пользователь задаёт сложный мастер-пароль при регистрации или первой инициализации профиля. Мастер-пароль не хранится на сервере ни в каком виде.</li>
<li>На основе этого пароля с помощью алгоритма <strong>PBKDF2 (600 000 итераций, HMAC-SHA256, соль "chepuhagram_salt")</strong> генерируется ключ шифрования резервной копии.</li> <li><strong>Деривация PBKDF2:</strong> Из пароля и предопределённой криптографической соли вычисляется производный ключ шифрования резервной копии. Используется стандарт PBKDF2 с хэш-функцией HMAC-SHA256 и <strong>600,000 итерациями</strong>. Такое число итераций делает невозможным брутфорс (перебор паролей) на графических процессорах и специализированных чипах.</li>
<li>Приватный ключ X25519 шифруется на этом ключе и отправляется на сервер как <code>encrypted_private_key</code>.</li> <li><strong>Шифрование приватного ключа:</strong> Локальный приватный ключ X25519 шифруется с помощью AES-256-GCM на деривированном ключе PBKDF2.</li>
<li>При входе на новом устройстве вы вводите мастер-пароль, приложение скачивает копию ключа, расшифровывает её на вашем устройстве, и вы снова можете читать все ваши чаты. Сервер видит только зашифрованный массив байт и не может его декодировать.</li> <li><strong>Сохранение копии:</strong> Зашифрованная строка приватного ключа (<code>encrypted_private_key</code>) отправляется на сервер. При авторизации на новом устройстве пользователь вводит свой мастер-пароль, приложение заново производит 600,000 итераций деривации ключа, скачивает защищённый контейнер с сервера и успешно расшифровывает приватный ключ локально.</li>
</ul> </ul>
<h2 id="sessionSecurityHeader">Безопасность сессий и автозавершение</h2> <h2 id="sessionSecurityHeader">5. Управление сессиями и синхронизация (Multi-Device Sync)</h2>
<p>Чепухаграм поддерживает одновременный вход на нескольких устройствах. Вся сессионная активность полностью подконтрольна пользователю:</p> <p>Чепухаграм спроектирован для бесшовной работы на нескольких устройствах одного пользователя. Это требует сложной логики синхронизации сообщений, прочтений и статусов через реалтайм WebSocket-каналы и локальную СУБД (Drift SQLite):</p>
<p>При любом изменении пароля сквозного шифрования (или при полном сбросе ключей) все остальные активные сессии на сторонних девайсах автоматически инвалидируются на сервере и прекращают работу. Это защищает ваши чаты от несанкционированного доступа с ранее авторизованных устройств.</p>
<h3>Доставка событий о прочтении (Read Receipts)</h3>
<p>При чтении переписки на одном устройстве, состояние непрочитанных сообщений синхронизируется на всех остальных девайсах пользователя с точностью до чата:</p>
<ul>
<li>Когда пользователь открывает чат или прокручивает его в самый низ (где виден последний элемент), клиент отправляет на сервер WebSocket-событие <code>read_all_chat</code> с указанием ID контакта.</li>
<li>Сервер обновляет статус всех непрочитанных сообщений в базе данных и рассылает WebSocket-пакеты <code>all_chat_read</code> всем активным сессиям (устройствам) читателя и автора сообщений.</li>
<li>Все устройства читателя мгновенно обновляют локальную базу данных сообщений, а также сбрасывают счётчик непрочитанных сообщений конкретного чата в провайдере `ContactProvider` в <code>0</code>, очищая бейджи непрочитанных в списке чатов.</li>
<li>Если сообщения прочитываются поштучно (по мере попадания в зону видимости экрана), отправляется событие <code>read_receipt</code>. Сервер вычисляет количество оставшихся непрочитанных сообщений **строго в рамках данного чата** и рассылает его устройствам, предотвращая некорректное суммирование глобального счётчика.</li>
</ul>
<h3>Смена мастер-пароля шифрования секретного ключа и отзыв авторизации (Force Logout)</h3>
<p>Выход из сессий на других устройствах происходит исключительно при смене мастер-пароля шифрования секретного ключа (или при полном сбросе ключей). Это гарантирует своевременное прекращение доступа к аккаунту при изменении ключевого секрета:</p>
<ol>
<li>Во время изменения мастер-пароля шифрования приватного ключа сервер определяет идентификатор сессии (<code>session_id</code>) устройства, инициировавшего операцию.</li>
<li>Сервер находит и удаляет все остальные активные сессии этого пользователя из таблицы <code>sessions</code> базы данных.</li>
<li>Для каждой отключенной сессии бэкенд вызывает метод <code>kill_session_socket</code> в менеджере WebSocket-соединений.</li>
<li>На эти устройства отправляется WebSocket-пакет <code>{"type": "session_terminated"}</code>, после чего сокеты принудительно закрываются.</li>
<li>Устройства, получившие данный сигнал, немедленно удаляют локальный файл базы данных сообщений (<code>chat_app.db</code>), токены авторизации, приватный ключ и возвращаются на экран входа.</li>
</ol>
<div class="callout" style="background: rgba(239, 83, 80, 0.05); border-left-color: #ef5350;">
<p><strong>Безопасность при выходе:</strong> При логауте база данных SQLite на компьютере или телефоне полностью стирается на уровне файловой системы. Это исключает возможность физического извлечения переписки с диска устройства после выхода из учётной записи.</p>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
<footer> <footer>
<div class="container"> <div class="container">
<a href="../index.html" class="footer-logo"> <a href="../" class="footer-logo">
<span class="logo-dot"></span>Чепухаграм <span class="logo-dot"></span>Чепухаграм
</a> </a>
<p class="footer-text">Документ подготовлен техническим отделом Чепухаграм. Все криптографические операции соответствуют современным стандартам безопасности.</p> <p class="footer-text">Документ подготовлен разработчиком Чепухаграм. Все алгоритмы и протоколы шифрования соответствуют спецификациям современных стандартов безопасности RFC 7748 и NIST SP 800-38D.</p>
</div> </div>
</footer> </footer>

View File

@ -731,8 +731,8 @@
</a> </a>
<nav id="navMenu"> <nav id="navMenu">
<a href="#features">Функции</a> <a href="#features">Функции</a>
<a href="./about-secutity/index.html">Безопасность</a> <a href="./about-secutity">Безопасность</a>
<a href="./about-secutity/index.html" class="btn-cta" id="ctaHeader">О защите</a> <a href="./about-secutity" class="btn-cta" id="ctaHeader">О защите</a>
</nav> </nav>
</div> </div>
</header> </header>
@ -744,7 +744,7 @@
<h1 class="hero-title" id="mainTitle">Чепухаграм</h1> <h1 class="hero-title" id="mainTitle">Чепухаграм</h1>
<p class="hero-subtitle" id="mainSub">Приватность следующего поколения. Полноценное сквозное шифрование сообщений, звонков и медиафайлов. Полный контроль над вашими личными данными.</p> <p class="hero-subtitle" id="mainSub">Приватность следующего поколения. Полноценное сквозное шифрование сообщений, звонков и медиафайлов. Полный контроль над вашими личными данными.</p>
<div class="hero-actions"> <div class="hero-actions">
<a href="./about-secutity/index.html" class="btn-cta" id="heroCta">Изучить защиту</a> <a href="./about-secutity" class="btn-cta" id="heroCta">Изучить защиту</a>
<a href="#features" class="btn-secondary" id="heroSecondary">Функции</a> <a href="#features" class="btn-secondary" id="heroSecondary">Функции</a>
</div> </div>
</div> </div>
@ -835,7 +835,7 @@
<span class="banner-tag">Архитектура безопасности</span> <span class="banner-tag">Архитектура безопасности</span>
<h2 class="banner-title" id="bannerTitle">Zero-Knowledge Безопасность</h2> <h2 class="banner-title" id="bannerTitle">Zero-Knowledge Безопасность</h2>
<p class="banner-text" id="bannerText">Мы придерживаемся принципа «нулевого разглашения». Ваши секретные ключи создаются на базе эллиптических кривых эллиптического протокола X25519 и никогда не покидают ваши устройства в незашифрованном виде.</p> <p class="banner-text" id="bannerText">Мы придерживаемся принципа «нулевого разглашения». Ваши секретные ключи создаются на базе эллиптических кривых эллиптического протокола X25519 и никогда не покидают ваши устройства в незашифрованном виде.</p>
<a href="./about-secutity/index.html" class="btn-cta" id="bannerCta">Подробнее о криптографии</a> <a href="./about-secutity" class="btn-cta" id="bannerCta">Подробнее о криптографии</a>
</div> </div>
<div class="banner-visual" id="lockIcon">🔒</div> <div class="banner-visual" id="lockIcon">🔒</div>
</div> </div>
@ -850,7 +850,7 @@
</a> </a>
<div class="footer-links"> <div class="footer-links">
<a href="#features">Функции</a> <a href="#features">Функции</a>
<a href="./about-secutity/index.html">Безопасность мессенджера</a> <a href="./about-secutity">Безопасность мессенджера</a>
</div> </div>
<p class="footer-text">© 2026 Чепухаграм. Все права защищены. Разработано с фокусом на абсолютную приватность.</p> <p class="footer-text">© 2026 Чепухаграм. Все права защищены. Разработано с фокусом на абсолютную приватность.</p>
</div> </div>