22-06-2026+12-47
This commit is contained in:
parent
5044cb6020
commit
6b7fd0531f
Binary file not shown.
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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,42 +2207,140 @@ 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 {
|
try {
|
||||||
setState(() {
|
// 1. Пробуем сначала скачать ZIP-архив
|
||||||
_downloadProgress = 0.0;
|
if (_latestApkZipUrl != null) {
|
||||||
_downloadedBytes = 0;
|
try {
|
||||||
_downloadTotalBytes = 0;
|
setState(() {
|
||||||
});
|
_downloadProgress = 0.0;
|
||||||
await Dio().download(
|
_downloadedBytes = 0;
|
||||||
_latestApkUrl!,
|
_downloadTotalBytes = 0;
|
||||||
path,
|
_downloadStatus = null;
|
||||||
cancelToken: _cancelToken,
|
});
|
||||||
onReceiveProgress: (rec, total) {
|
print("Попытка скачать ZIP обновление с $_latestApkZipUrl");
|
||||||
|
await Dio().download(
|
||||||
|
_latestApkZipUrl!,
|
||||||
|
zipPath,
|
||||||
|
cancelToken: _cancelToken,
|
||||||
|
onReceiveProgress: (rec, total) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_downloadedBytes = rec;
|
||||||
|
_downloadTotalBytes = total > 0 ? total : 0;
|
||||||
|
_downloadProgress = total > 0 ? rec / total : 0.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_downloadedBytes = rec;
|
_downloadStatus = "Распаковка...";
|
||||||
_downloadTotalBytes = total > 0 ? total : 0;
|
_downloadProgress = 0.0;
|
||||||
_downloadProgress = total > 0 ? rec / total : 0.0;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
print("Начало распаковки ZIP...");
|
||||||
await OpenFilex.open(path);
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
18
pubspec.lock
18
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
10
srv/main.py
10
srv/main.py
|
|
@ -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 не найден")
|
||||||
|
|
|
||||||
|
|
@ -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<String> 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<String> decryptMessage(String base64Data, SecretKey sharedKey) async {
|
Future<String> 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">Остаток (<64 КБ)</span>
|
<span class="block-title">Хвостовой Блок (< 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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue