21-06-2026+22-50

This commit is contained in:
Artur 2026-06-21 22:50:08 +05:00
parent 4363fdf699
commit 680771f75e
7 changed files with 1726 additions and 17 deletions

View File

@ -60,24 +60,33 @@ class CryptoService {
String encryptedPrivateKey,
String masterPassword,
) async {
final encryptedData = base64Decode(encryptedPrivateKey);
// Разделяем nonce и зашифрованные данные
final nonce = encryptedData.sublist(0, 12); // GCM nonce = 12 bytes
final macBytes = encryptedData.sublist(12, 28);
final cipherText = encryptedData.sublist(28);
// 1. Попытка дешифрации с новым значением итераций (600 000)
try {
final encryptedData = base64Decode(encryptedPrivateKey);
// Разделяем nonce и зашифрованные данные
final nonce = encryptedData.sublist(0, 12); // GCM nonce = 12 bytes
final macBytes = encryptedData.sublist(12, 28);
final cipherText = encryptedData.sublist(28);
final masterKey = await _deriveKeyFromPassword(masterPassword);
final masterKey = await _deriveKeyFromPassword(masterPassword, iterations: 600000);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: masterKey,
);
return base64Encode(decrypted);
} catch (e) {
throw Exception('Неверный мастер-пароль или поврежденные данные');
} catch (_) {
// 2. Если не удалось, пробуем старое значение (10 000) для обратной совместимости
try {
final masterKey = await _deriveKeyFromPassword(masterPassword, iterations: 10000);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: masterKey,
);
return base64Encode(decrypted);
} catch (e) {
throw Exception('Неверный мастер-пароль или поврежденные данные');
}
}
}
@ -98,10 +107,10 @@ class CryptoService {
return base64Encode(encryptedData);
}
Future<SecretKey> _deriveKeyFromPassword(String password) async {
Future<SecretKey> _deriveKeyFromPassword(String password, {int iterations = 600000}) async {
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac.sha256(),
iterations: 10000,
iterations: iterations,
bits: 256,
);

View File

@ -489,6 +489,11 @@ class AuthProvider extends ChangeNotifier {
// Метод для начала с чистого листа (новые ключи)
Future<void> resetKeys() async {
try {
await ApiService().clearOtherSessions();
} catch (e) {
print("Error clearing other sessions on key reset: $e");
}
await _storage.delete(key: 'private_key');
try {
final allKeys = await _storage.readAll();

View File

@ -1535,7 +1535,7 @@ class _ChatScreenState extends State<ChatScreen> with RouteAware {
sharedSecret,
);
final content50 = newText.length > 50 ? newText.substring(0, 50) : newText;
final content50 = newText.length > 500 ? newText.substring(0, 500) : newText;
final encryptedContent50 = await _cryptoService.encryptMessage(
content50,
sharedSecret,
@ -2726,7 +2726,7 @@ class _ChatScreenState extends State<ChatScreen> with RouteAware {
);
String previewText = rawText.isNotEmpty ? rawText : "[Фото]";
if (previewText.length > 50) previewText = previewText.substring(0, 50);
if (previewText.length > 500) previewText = previewText.substring(0, 500);
encryptedContent50 = await _cryptoService.encryptMessage(
previewText,
sharedSecret,

View File

@ -175,6 +175,12 @@ class _SecuritySettingsScreenState extends State<SecuritySettingsScreen> {
);
if (!success) throw Exception('Сервер отклонил обновление крипто-ключа.');
try {
await ApiService().clearOtherSessions();
} catch (e) {
print("Error clearing other sessions on encryption password change: $e");
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@ -509,6 +515,30 @@ class _SecuritySettingsScreenState extends State<SecuritySettingsScreen> {
},
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.warning_amber_rounded,
color: Theme.of(context).colorScheme.error,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Предупреждение: при изменении пароля шифрования все остальные активные сессии на других устройствах будут автоматически завершены.',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton(

View File

@ -37,7 +37,7 @@ async def send_system_notification(db: AsyncSession, receiver_id: int, plain_tex
# 2. Шифруем текст сообщения, используя сохраненный ключ
encrypted_content = encrypt_system_message(plain_text, u_public_key)
preview_text = plain_text if len(plain_text) <= 50 else f"{plain_text[:47]}..."
preview_text = plain_text if len(plain_text) <= 500 else f"{plain_text[:497]}..."
encrypted_preview = encrypt_system_message(preview_text, u_public_key)
# 3. Записываем сообщение в историю базы данных

View File

@ -0,0 +1,719 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Архитектура Безопасности и Шифрования — Чепухаграм</title>
<meta name="description" content="Подробный разбор сквозного шифрования (E2EE) мессенджера Чепухаграм. Узнайте об использовании протоколов X25519, AES-256-GCM, PBKDF2 и поблочной защите медиафайлов.">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b071a;
--bg-card: #14102d;
--accent-color: #7c4dff;
--accent-light: #9e75ff;
--accent-glow: rgba(124, 77, 255, 0.3);
--text-primary: #ffffff;
--text-secondary: #94a3b8;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--code-bg: #110c26;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
overflow-x: hidden;
line-height: 1.75;
}
h1, h2, h3, h4, .logo {
font-family: 'Outfit', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-color);
}
::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 4px;
transition: background 0.3s;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
.container {
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: 0 24px;
}
/* Header Navigation */
header {
background: rgba(11, 7, 26, 0.8);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--glass-border);
padding: 16px 0;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
}
.logo-dot {
width: 8px;
height: 8px;
background-color: var(--accent-color);
border-radius: 50%;
box-shadow: 0 0 10px var(--accent-color);
}
.btn-back {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.3s;
display: flex;
align-items: center;
gap: 6px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
padding: 8px 16px;
border-radius: 20px;
}
.btn-back:hover {
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.06);
}
/* Hero Section */
.hero {
padding: 80px 0 40px 0;
background: radial-gradient(circle at 50% -20%, rgba(124, 77, 255, 0.12) 0%, transparent 60%);
}
.hero h1 {
font-size: 46px;
font-weight: 700;
margin-bottom: 16px;
line-height: 1.2;
background: linear-gradient(135deg, #ffffff 40%, #d1c4e9 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-meta {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 40px;
}
/* Article Content */
.content-section {
padding-bottom: 80px;
}
.intro-text {
font-size: 18px;
font-weight: 300;
color: #e2e8f0;
margin-bottom: 40px;
line-height: 1.8;
}
h2 {
font-size: 28px;
font-weight: 700;
margin: 54px 0 24px 0;
border-left: 4px solid var(--accent-color);
padding-left: 16px;
color: #f8fafc;
}
h3 {
font-size: 20px;
font-weight: 600;
margin: 32px 0 16px 0;
color: #f1f5f9;
}
p {
margin-bottom: 24px;
color: #cbd5e1;
}
ul, ol {
margin-bottom: 28px;
padding-left: 24px;
color: #cbd5e1;
}
li {
margin-bottom: 10px;
}
/* Tech Spec Grid */
.spec-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin: 40px 0;
}
.spec-card {
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 28px 24px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
transition: transform 0.3s;
}
.spec-card:hover {
transform: translateY(-4px);
}
.spec-card .icon {
font-size: 32px;
margin-bottom: 16px;
display: inline-block;
}
.spec-card h3 {
font-size: 16px;
color: var(--text-primary);
margin: 0 0 8px 0;
font-weight: 600;
}
.spec-card p {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 0;
line-height: 1.5;
}
/* Cryptographic Exchange Diagram (CSS representation) */
.dh-diagram {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 36px 30px;
margin: 40px 0;
gap: 20px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.dh-entity {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 32%;
}
.dh-avatar {
width: 54px;
height: 54px;
background: linear-gradient(135deg, var(--accent-color), #b388ff);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: white;
font-size: 15px;
box-shadow: 0 4px 15px var(--accent-glow);
}
.dh-avatar.bob {
background: linear-gradient(135deg, #00b0ff, #00e5ff);
box-shadow: 0 4px 15px rgba(0, 176, 255, 0.3);
}
.dh-key {
padding: 8px 12px;
border-radius: 8px;
font-size: 11px;
text-align: center;
width: 100%;
font-weight: 500;
font-family: 'Fira Code', monospace;
}
.dh-key.private {
background: rgba(239, 83, 80, 0.12);
border: 1px solid rgba(239, 83, 80, 0.25);
color: #ef5350;
}
.dh-key.public {
background: rgba(76, 217, 100, 0.12);
border: 1px solid rgba(76, 217, 100, 0.25);
color: #4cd964;
}
.dh-key.shared {
background: rgba(124, 77, 255, 0.15);
border: 1px solid rgba(124, 77, 255, 0.3);
color: #b388ff;
font-size: 12px;
font-weight: 600;
}
.dh-channel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
font-size: 11px;
color: var(--text-secondary);
}
.dh-arrow {
background: rgba(255, 255, 255, 0.04);
padding: 6px 12px;
border-radius: 20px;
border: 1px solid var(--glass-border);
width: 100%;
text-align: center;
font-weight: 500;
}
/* File Block Stream Encryption Diagram */
.block-stream-diagram {
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg-card);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 36px 30px;
margin: 40px 0;
gap: 20px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.stream-file {
background: linear-gradient(135deg, var(--accent-color) 0%, #5e35b1 100%);
padding: 12px 36px;
border-radius: 12px;
font-weight: 600;
box-shadow: 0 4px 15px var(--accent-glow);
font-size: 14px;
}
.stream-split {
font-size: 12px;
color: var(--text-secondary);
font-style: italic;
}
.stream-blocks {
display: flex;
gap: 16px;
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
.stream-block {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 18px;
flex: 1;
min-width: 220px;
text-align: center;
}
.stream-block.tail {
border-style: dashed;
border-color: rgba(255, 255, 255, 0.2);
}
.block-title {
font-size: 13px;
font-weight: 600;
display: block;
margin-bottom: 8px;
color: #fff;
}
.block-crypto {
font-size: 11px;
color: var(--accent-light);
margin-bottom: 10px;
font-weight: 500;
}
.block-result {
font-size: 11px;
color: var(--text-secondary);
background: rgba(0, 0, 0, 0.25);
padding: 8px 10px;
border-radius: 8px;
line-height: 1.4;
font-family: 'Fira Code', monospace;
}
/* Encryption Steps Visualizer */
.steps {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 40px;
margin: 40px 0;
position: relative;
}
.step-item {
display: flex;
gap: 24px;
margin-bottom: 32px;
position: relative;
}
.step-item:last-child {
margin-bottom: 0;
}
.step-item:not(:last-child)::after {
content: '';
position: absolute;
top: 36px;
left: 18px;
width: 2px;
height: calc(100% - 4px);
background: var(--glass-border);
}
.step-num {
width: 38px;
height: 38px;
background: var(--accent-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 15px;
flex-shrink: 0;
box-shadow: 0 0 10px var(--accent-glow);
}
.step-content h3 {
font-size: 18px;
margin: 0 0 8px 0;
font-weight: 600;
}
.step-content p {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 0;
}
/* Code Block Container */
.code-container {
background: var(--code-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 24px;
margin: 32px 0;
overflow-x: auto;
position: relative;
}
.code-container::before {
content: 'Dart / PointyCastle / Cryptography';
position: absolute;
top: 8px;
right: 16px;
font-size: 10px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
}
code {
font-family: 'Fira Code', monospace;
font-size: 13px;
color: #e2e8f0;
}
/* Callout Box */
.callout {
background: rgba(124, 77, 255, 0.08);
border-left: 4px solid var(--accent-color);
padding: 20px 24px;
border-radius: 0 16px 16px 0;
margin: 32px 0;
}
.callout p {
margin-bottom: 0;
font-size: 14px;
color: #e2e8f0;
}
/* Footer */
footer {
border-top: 1px solid var(--glass-border);
padding: 45px 0;
text-align: center;
background: #06040e;
}
.footer-logo {
font-size: 18px;
font-weight: 700;
color: white;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
@media (max-width: 768px) {
.dh-diagram {
flex-direction: column;
gap: 30px;
padding: 30px 20px;
}
.dh-entity {
width: 100%;
}
.dh-channel {
width: 100%;
}
.block-stream-diagram {
padding: 30px 20px;
}
.stream-blocks {
flex-direction: column;
}
.hero h1 {
font-size: 34px;
}
}
</style>
</head>
<body>
<!-- Header Navigation -->
<header>
<div class="container header-content">
<a href="../index.html" class="logo" id="headerLogo">
<span class="logo-dot"></span>Чепухаграм
</a>
<a href="../index.html" class="btn-back" id="backLink">
На главную
</a>
</div>
</header>
<!-- Hero Header -->
<section class="hero">
<div class="container">
<h1 id="titleHeader">Безопасность и сквозное шифрование (E2EE)</h1>
<div class="hero-meta" id="metaDate">Обновлено: Июнь 2026 • Документация Чепухаграм</div>
<p class="intro-text" id="introDesc">В мессенджере Чепухаграм конфиденциальность переписки обеспечивается математическими законами. Никакие третьи лица, включая разработчиков и администраторов серверов, не могут получить доступ к содержимому ваших чатов.</p>
</div>
</section>
<!-- Main Content -->
<div class="container content-section">
<!-- Technical Specifications -->
<div class="spec-grid" id="specGrid">
<div class="spec-card">
<span class="icon">🔑</span>
<h3>X25519</h3>
<p>Протокол Диффи-Хеллмана для генерации общего ключа (ECDH)</p>
</div>
<div class="spec-card">
<span class="icon">⚙️</span>
<h3>AES-256-GCM</h3>
<p>Симметричное шифрование с проверкой целостности данных</p>
</div>
<div class="spec-card">
<span class="icon">🔒</span>
<h3>PBKDF2</h3>
<p>Криптографическая деривация ключей на базе мастер-пароля (600,000 ит.)</p>
</div>
</div>
<h2 id="e2eeHeader">Принцип сквозного шифрования (E2EE)</h2>
<p>Шифрование «end-to-end» означает, что шифрование информации происходит непосредственно на устройстве отправителя, а дешифрование — только на устройстве получателя. Серверная часть выполняет лишь функцию почтальона — пересылает зашифрованные пакеты байт, не имея ключей для их расшифровки.</p>
<!-- ECDH Exchange Visual Diagram -->
<h3>Схема согласования ключей (X25519)</h3>
<div class="dh-diagram">
<div class="dh-entity">
<div class="dh-avatar">Вы</div>
<div class="dh-key private">Приватный ключ A</div>
<div class="dh-key public">Публичный ключ A</div>
<div class="dh-key shared">Общий секрет (K)</div>
</div>
<div class="dh-channel">
<div class="dh-arrow">публичный ключ A →</div>
<div class="dh-arrow">← публичный ключ B</div>
<p style="margin: 8px 0 0 0; text-align: center; font-size: 11px;">(сервер пересылает только публичные ключи)</p>
</div>
<div class="dh-entity">
<div class="dh-avatar bob">Собеседник</div>
<div class="dh-key private">Приватный ключ B</div>
<div class="dh-key public">Публичный ключ B</div>
<div class="dh-key shared">Общий секрет (K)</div>
</div>
</div>
<div class="steps" id="stepsSection">
<div class="step-item">
<div class="step-num">1</div>
<div class="step-content">
<h3>Создание пары ключей</h3>
<p>При первом запуске приложения Чепухаграм генерирует на устройстве пару асимметричных ключей X25519 (приватный и публичный). Публичный ключ отправляется на сервер для того, чтобы другие пользователи могли начать с вами чат.</p>
</div>
</div>
<div class="step-item">
<div class="step-num">2</div>
<div class="step-content">
<h3>Деривация общего секрета (Shared Secret)</h3>
<p>Когда вы открываете чат, приложение берет ваш приватный ключ X25519 и публичный ключ собеседника, вычисляя общий секрет по алгоритму ECDH. Этот секретный ключ никогда не передается по сети — обе стороны вычисляют его независимо на своих девайсах.</p>
</div>
</div>
<div class="step-item">
<div class="step-num">3</div>
<div class="step-content">
<h3>Симметричное шифрование</h3>
<p>Все отправляемые текстовые сообщения шифруются по стандарту AES-256-GCM с уникальным вектором инициализации (Nonce). Полученный шифротекст передается на сервер.</p>
</div>
</div>
</div>
<h2 id="messagesCryptoHeader">Шифрование текстовых сообщений</h2>
<p>Каждое текстовое сообщение кодируется в массив байтов, после чего для него генерируется случайный 12-байтовый вектор инициализации (nonce). Данные шифруются на ключе sharedSecret по алгоритму AES-256-GCM. Результат представляет собой склеенный массив байтов: <code>nonce (12 байт) + mac (16 байт) + ciphertext (зашифрованный текст)</code>, закодированный в Base64.</p>
<div class="code-container" id="messageCode">
<pre><code>// Схематичный пример дешифровки сообщения на клиенте
Future&lt;String&gt; decryptMessage(String base64Data, SecretKey sharedKey) async {
final data = base64Decode(base64Data);
final nonce = data.sublist(0, 12);
final mac = data.sublist(12, 28);
final cipherText = data.sublist(28);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
secretKey: sharedKey,
);
return utf8.decode(decrypted);
}</code></pre>
</div>
<h2 id="filesCryptoHeader">Поблочное шифрование медиафайлов</h2>
<p>Для предотвращения утечек данных при отправке медиафайлов (картинок, видео, голосовых заметок и документов), в Чепухаграм внедрена продвинутая поблочная система шифрования:</p>
<ol>
<li>При выборе файла генерируется случайный симметричный ключ файла (<code>fileKey</code>).</li>
<li>Ключ <code>fileKey</code> шифруется на общем ключе чата (<code>sharedSecret</code>) и отправляется на сервер как поле <code>encrypted_key</code>.</li>
<li>Сам файл считывается потоком и шифруется блоками по 64 КБ с использованием <code>fileKey</code>. К каждому зашифрованному блоку добавляется 4-байтовый заголовок, содержащий точную длину блока, и уникальный вектор инициализации с тегом аутентификации MAC.</li>
</ol>
<!-- Media block stream diagram -->
<h3>Архитектура поблочного крипто-стрима</h3>
<div class="block-stream-diagram">
<div class="stream-file">Исходный файл медиа</div>
<div class="stream-split">👇 Считывание блоками по 64 КБ</div>
<div class="stream-blocks">
<div class="stream-block">
<span class="block-title">Блок 1 (64 КБ)</span>
<div class="block-crypto">🔒 AES-256-GCM (fileKey)</div>
<div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div>
</div>
<div class="stream-block">
<span class="block-title">Блок 2 (64 КБ)</span>
<div class="block-crypto">🔒 AES-256-GCM (fileKey)</div>
<div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div>
</div>
<div class="stream-block tail">
<span class="block-title">Остаток (&lt;64 КБ)</span>
<div class="block-crypto">🔒 AES-256-GCM (fileKey)</div>
<div class="block-result">Длина (4б) + Nonce (12б) + Данные + MAC (16б)</div>
</div>
</div>
</div>
<div class="callout" id="fileSecurityNote">
<p><strong>Важно:</strong> Такой подход позволяет осуществлять потоковую дешифрацию медиа во время загрузки (стриминг), благодаря чему видео и аудиозаписи начинают воспроизводиться еще до полной загрузки файла.</p>
</div>
<h2 id="masterPassHeader">Резервная копия ключей и мастер-пароль</h2>
<p>Поскольку приватный ключ X25519 хранится в изолированном защищенном хранилище (Secure Storage) вашего смартфона, при переустановке приложения вы можете потерять доступ к переписке. Для предотвращения этого в Чепухаграм создана система защищенных резервных копий:</p>
<ul>
<li>Вы придумываете сложный <strong>Мастер-пароль</strong>.</li>
<li>На основе этого пароля с помощью алгоритма <strong>PBKDF2 (600 000 итераций, HMAC-SHA256, соль "chepuhagram_salt")</strong> генерируется ключ шифрования резервной копии.</li>
<li>Приватный ключ X25519 шифруется на этом ключе и отправляется на сервер как <code>encrypted_private_key</code>.</li>
<li>При входе на новом устройстве вы вводите мастер-пароль, приложение скачивает копию ключа, расшифровывает её на вашем устройстве, и вы снова можете читать все ваши чаты. Сервер видит только зашифрованный массив байт и не может его декодировать.</li>
</ul>
<h2 id="sessionSecurityHeader">Безопасность сессий и автозавершение</h2>
<p>Чепухаграм поддерживает одновременный вход на нескольких устройствах. Вся сессионная активность полностью подконтрольна пользователю:</p>
<p>При любом изменении пароля сквозного шифрования (или при полном сбросе ключей) все остальные активные сессии на сторонних девайсах автоматически инвалидируются на сервере и прекращают работу. Это защищает ваши чаты от несанкционированного доступа с ранее авторизованных устройств.</p>
</div>
<!-- Footer -->
<footer>
<div class="container">
<a href="../index.html" class="footer-logo">
<span class="logo-dot"></span>Чепухаграм
</a>
<p class="footer-text">Документ подготовлен техническим отделом Чепухаграм. Все криптографические операции соответствуют современным стандартам безопасности.</p>
</div>
</footer>
</body>
</html>

946
srv/site/index.html Normal file
View File

@ -0,0 +1,946 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Чепухаграм — Безопасное общение без компромиссов</title>
<meta name="description" content="Чепухаграм — современный, премиальный мессенджер со сквозным шифрованием (E2EE). Общайтесь безопасно, обменивайтесь файлами, совершайте WebRTC звонки и отправляйте видео-кружочки.">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b071a;
--accent-color: #7c4dff;
--accent-light: #9e75ff;
--accent-glow: rgba(124, 77, 255, 0.3);
--text-primary: #ffffff;
--text-secondary: #94a3b8;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--card-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5);
--phone-bg: #130f26;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
overflow-x: hidden;
line-height: 1.6;
}
h1, h2, h3, .logo {
font-family: 'Outfit', sans-serif;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-color);
}
::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 4px;
transition: background 0.3s;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
/* Layout Container */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* Navigation Header */
header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
background: rgba(11, 7, 26, 0.75);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--glass-border);
padding: 16px 0;
transition: all 0.3s;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
}
.logo-dot {
width: 10px;
height: 10px;
background-color: var(--accent-color);
border-radius: 50%;
box-shadow: 0 0 12px var(--accent-color);
display: inline-block;
}
nav {
display: flex;
align-items: center;
gap: 32px;
}
nav a {
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
font-size: 15px;
transition: color 0.3s;
}
nav a:hover {
color: var(--text-primary);
}
.btn-cta {
background: linear-gradient(135deg, var(--accent-color) 0%, #5e35b1 100%);
color: white;
padding: 10px 24px;
border-radius: 30px;
font-weight: 600;
font-size: 14px;
text-decoration: none;
transition: transform 0.3s, box-shadow 0.3s;
box-shadow: 0 4px 15px var(--accent-glow);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-cta:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(124, 77, 255, 0.5);
}
/* Hero Section */
.hero {
padding: 180px 0 100px 0;
position: relative;
background: radial-gradient(circle at 10% 20%, rgba(124, 77, 255, 0.06) 0%, transparent 50%);
}
/* Floating Background Glow Effect */
.hero::after {
content: '';
position: absolute;
top: 25%;
right: 12%;
width: 450px;
height: 450px;
background: radial-gradient(circle, rgba(124, 77, 255, 0.1) 0%, rgba(186, 104, 200, 0.03) 50%, transparent 70%);
z-index: -1;
filter: blur(60px);
pointer-events: none;
animation: floatGlow 8s infinite alternate ease-in-out;
}
@keyframes floatGlow {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(40px, -40px) scale(1.15); }
}
.hero-layout {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
align-items: center;
gap: 60px;
}
.hero-title {
font-size: 56px;
font-weight: 700;
line-height: 1.15;
margin-bottom: 24px;
background: linear-gradient(135deg, #ffffff 30%, #d1c4e9 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 40px;
font-weight: 300;
max-width: 580px;
}
.hero-actions {
display: flex;
gap: 16px;
}
.btn-secondary {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-primary);
padding: 12px 28px;
border-radius: 30px;
font-weight: 600;
text-decoration: none;
transition: background 0.3s, border-color 0.3s;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
}
/* Mockup Mobile Phone styling */
.hero-mockup-wrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.hero-mockup {
width: 100%;
max-width: 380px;
height: 600px;
background: var(--phone-bg);
border: 12px solid #2d264d;
border-radius: 40px;
box-shadow: var(--card-shadow), 0 0 0 1px rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* Phone Camera Notch */
.hero-mockup::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 130px;
height: 25px;
background: #2d264d;
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
z-index: 10;
}
/* Status Bar */
.mockup-status {
display: flex;
justify-content: space-between;
align-items: center;
padding: 26px 20px 10px 24px;
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
font-weight: 600;
letter-spacing: 0.2px;
z-index: 5;
}
.mockup-status-icons {
display: flex;
gap: 6px;
align-items: center;
}
.mockup-header {
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--glass-border);
padding: 10px 20px 15px 20px;
z-index: 5;
background: rgba(19, 15, 38, 0.85);
backdrop-filter: blur(10px);
}
.mockup-avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent-color), #b388ff);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: white;
font-size: 14px;
}
.mockup-info h4 {
font-size: 13px;
font-weight: 600;
}
.mockup-info span {
font-size: 10px;
color: #4cd964;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.mockup-info span::before {
content: '';
display: inline-block;
width: 5px;
height: 5px;
background-color: #4cd964;
border-radius: 50%;
}
/* Chat Messages Container */
.mockup-chat {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
z-index: 1;
}
.mockup-bubble {
max-width: 80%;
padding: 10px 14px;
border-radius: 16px;
font-size: 13px;
line-height: 1.4;
position: relative;
word-wrap: break-word;
}
.mockup-bubble.partner {
background: #231b40;
align-self: flex-start;
border-bottom-left-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.02);
color: #e2e8f0;
}
.mockup-bubble.me {
background: var(--accent-color);
color: white;
align-self: flex-end;
border-bottom-right-radius: 3px;
box-shadow: 0 4px 12px var(--accent-glow);
}
.mockup-bubble .time {
font-size: 9px;
color: rgba(255, 255, 255, 0.4);
text-align: right;
margin-top: 4px;
display: block;
}
/* Typing Indicator CSS */
.typing-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
align-self: flex-start;
background: #231b40;
border-radius: 16px;
border-bottom-left-radius: 3px;
}
.typing-indicator span {
width: 6px;
height: 6px;
background-color: #a0aec0;
border-radius: 50%;
animation: typing-bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing-bounce {
0%, 80%, 100% { transform: scale(0.3); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
/* Chat Input Field Mockup */
.mockup-input {
display: flex;
align-items: center;
gap: 10px;
border-top: 1px solid var(--glass-border);
padding: 12px 15px 24px 15px;
z-index: 5;
background: rgba(19, 15, 38, 0.95);
}
.mockup-input-field {
flex: 1;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 8px 16px;
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mockup-send {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--accent-color) 0%, #6200ea 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 8px var(--accent-glow);
}
/* Phone Home Indicator Bar */
.mockup-home-bar {
position: absolute;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
z-index: 10;
}
/* Features Section */
.features {
padding: 120px 0;
position: relative;
background: radial-gradient(circle at 90% 80%, rgba(124, 77, 255, 0.04) 0%, transparent 50%);
}
.section-header {
text-align: center;
margin-bottom: 70px;
}
.section-title {
font-size: 40px;
font-weight: 700;
margin-bottom: 16px;
background: linear-gradient(135deg, #ffffff 40%, #e2d9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.section-subtitle {
font-size: 16px;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
font-weight: 300;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 30px;
}
.feature-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 36px;
transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1), border-color 0.4s, box-shadow 0.4s;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top left, rgba(124, 77, 255, 0.15) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.4s;
pointer-events: none;
}
.feature-card:hover {
transform: translateY(-6px);
border-color: rgba(124, 77, 255, 0.25);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 20px rgba(124, 77, 255, 0.08);
}
.feature-card:hover::before {
opacity: 1;
}
.feature-icon {
width: 52px;
height: 52px;
background: rgba(124, 77, 255, 0.08);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-light);
margin-bottom: 24px;
font-size: 24px;
border: 1px solid rgba(124, 77, 255, 0.15);
}
.feature-card h3 {
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
}
.feature-card p {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
/* E2EE Call-to-Action Card */
.e2ee-banner {
padding: 60px 0 120px 0;
}
.banner-card {
background: linear-gradient(135deg, rgba(124, 77, 255, 0.12) 0%, rgba(20, 16, 35, 0.4) 100%);
border: 1px solid rgba(124, 77, 255, 0.18);
border-radius: 32px;
padding: 60px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
box-shadow: var(--card-shadow);
position: relative;
overflow: hidden;
}
.banner-card::after {
content: '';
position: absolute;
bottom: -50px;
right: -50px;
width: 250px;
height: 250px;
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
pointer-events: none;
}
.banner-content {
max-width: 600px;
z-index: 1;
}
.banner-tag {
background: rgba(124, 77, 255, 0.15);
color: #d1c4e9;
padding: 6px 14px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
display: inline-block;
margin-bottom: 18px;
border: 1px solid rgba(124, 77, 255, 0.2);
}
.banner-title {
font-size: 36px;
font-weight: 700;
margin-bottom: 16px;
background: linear-gradient(135deg, #ffffff 40%, #e2d9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.banner-text {
color: var(--text-secondary);
font-size: 15px;
margin-bottom: 32px;
line-height: 1.7;
}
.banner-visual {
font-size: 120px;
color: var(--accent-color);
opacity: 0.85;
filter: drop-shadow(0 0 20px var(--accent-glow));
animation: pulse 3.5s infinite ease-in-out;
z-index: 1;
user-select: none;
}
@keyframes pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 20px var(--accent-glow)); }
50% { transform: scale(1.06); filter: drop-shadow(0 0 35px rgba(124, 77, 255, 0.6)); }
100% { transform: scale(1); filter: drop-shadow(0 0 20px var(--accent-glow)); }
}
/* Footer */
footer {
border-top: 1px solid var(--glass-border);
padding: 50px 0;
text-align: center;
background: #06040e;
}
.footer-logo {
font-size: 20px;
font-weight: 700;
margin-bottom: 16px;
text-decoration: none;
color: white;
display: inline-flex;
align-items: center;
gap: 8px;
}
.footer-links {
display: flex;
justify-content: center;
gap: 28px;
margin-bottom: 24px;
}
.footer-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
transition: color 0.3s;
}
.footer-links a:hover {
color: var(--text-primary);
}
.footer-text {
font-size: 12px;
color: rgba(255, 255, 255, 0.25);
}
/* Responsive Styles */
@media (max-width: 900px) {
.hero-layout {
grid-template-columns: 1fr;
text-align: center;
gap: 50px;
}
.hero-subtitle {
margin-left: auto;
margin-right: auto;
}
.hero-actions {
justify-content: center;
}
.hero-mockup {
max-width: 360px;
height: 570px;
}
.banner-card {
flex-direction: column;
text-align: center;
padding: 40px;
}
.banner-visual {
font-size: 100px;
}
}
@media (max-width: 600px) {
header {
padding: 12px 0;
}
nav {
gap: 16px;
}
nav a {
font-size: 13px;
}
.btn-cta {
padding: 8px 16px;
font-size: 12px;
}
.logo {
font-size: 20px;
}
.hero-title {
font-size: 40px;
}
.hero-subtitle {
font-size: 15px;
}
.section-title {
font-size: 32px;
}
}
</style>
</head>
<body>
<!-- Header Navigation -->
<header>
<div class="container header-content">
<a href="#" class="logo" id="logoLink">
<span class="logo-dot"></span>Чепухаграм
</a>
<nav id="navMenu">
<a href="#features">Функции</a>
<a href="./about-secutity/index.html">Безопасность</a>
<a href="./about-secutity/index.html" class="btn-cta" id="ctaHeader">О защите</a>
</nav>
</div>
</header>
<!-- Hero Section -->
<section class="hero">
<div class="container hero-layout">
<div class="hero-text">
<h1 class="hero-title" id="mainTitle">Чепухаграм</h1>
<p class="hero-subtitle" id="mainSub">Приватность следующего поколения. Полноценное сквозное шифрование сообщений, звонков и медиафайлов. Полный контроль над вашими личными данными.</p>
<div class="hero-actions">
<a href="./about-secutity/index.html" class="btn-cta" id="heroCta">Изучить защиту</a>
<a href="#features" class="btn-secondary" id="heroSecondary">Функции</a>
</div>
</div>
<!-- Interactive Chat Mockup (Mobile phone styling) -->
<div class="hero-mockup-wrapper">
<div class="hero-mockup" id="chatMockup">
<div class="mockup-status">
<div class="mockup-time">09:41</div>
<div class="mockup-status-icons">📶 🔋</div>
</div>
<div class="mockup-header">
<div class="mockup-avatar">А</div>
<div class="mockup-info">
<h4>Алиса</h4>
<span>в сети</span>
</div>
</div>
<div class="mockup-chat" id="mockupChat">
<div class="mockup-bubble partner">
Привет! Ты проверил систему шифрования Чепухаграм?
<span class="time">22:04</span>
</div>
<div class="mockup-bubble me">
Да! Используется X25519 для обмена ключами и AES-256-GCM для шифрования.
<span class="time">22:05</span>
</div>
<div class="mockup-bubble partner">
Отлично! А что с файлами и звонками?
<span class="time">22:05</span>
</div>
</div>
<div class="mockup-input">
<div class="mockup-input-field" id="mockupInputText">Напишите сообщение...</div>
<div class="mockup-send" id="mockupSendBtn"></div>
</div>
<div class="mockup-home-bar"></div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="features" id="features">
<div class="container">
<div class="section-header">
<h2 class="section-title" id="featuresTitle">Возможности Чепухаграм</h2>
<p class="section-subtitle" id="featuresSub">Сочетание передовых криптографических технологий с удобным современным интерфейсом мессенджера.</p>
</div>
<div class="features-grid">
<!-- Feature 1: E2EE Messages -->
<div class="feature-card" id="featCard1">
<div class="feature-icon">🔒</div>
<h3>Сквозное шифрование</h3>
<p>Текстовые сообщения и метаданные шифруются прямо на вашем устройстве и передаются в зашифрованном виде. Сервер не имеет ключей для расшифровки.</p>
</div>
<!-- Feature 2: Voice & Video notes -->
<div class="feature-card" id="featCard2">
<div class="feature-icon">🎙️</div>
<h3>Голос и Видео-кружочки</h3>
<p>Записывайте голосовые сообщения и видео-кружочки в одно касание. Нативное сжатие через FFmpeg уменьшает вес медиа без потери качества.</p>
</div>
<!-- Feature 3: File Streaming -->
<div class="feature-card" id="featCard3">
<div class="feature-icon">📤</div>
<h3>Стриминг файлов</h3>
<p>Безопасный поблочный стриминг файлов любого формата. Каждый файл шифруется своим уникальным AES-GCM ключом, который передается через E2EE канал.</p>
</div>
<!-- Feature 4: WebRTC Calls -->
<div class="feature-card" id="featCard4">
<div class="feature-icon">📞</div>
<h3>Аудио и видеозвонки</h3>
<p>Кристально чистые и конфиденциальные WebRTC звонки напрямую между вашими устройствами в обход промежуточных серверов.</p>
</div>
</div>
</div>
</section>
<!-- Encryption Banner Section -->
<section class="e2ee-banner">
<div class="container">
<div class="banner-card" id="bannerCard">
<div class="banner-content">
<span class="banner-tag">Архитектура безопасности</span>
<h2 class="banner-title" id="bannerTitle">Zero-Knowledge Безопасность</h2>
<p class="banner-text" id="bannerText">Мы придерживаемся принципа «нулевого разглашения». Ваши секретные ключи создаются на базе эллиптических кривых эллиптического протокола X25519 и никогда не покидают ваши устройства в незашифрованном виде.</p>
<a href="./about-secutity/index.html" class="btn-cta" id="bannerCta">Подробнее о криптографии</a>
</div>
<div class="banner-visual" id="lockIcon">🔒</div>
</div>
</div>
</section>
<!-- Footer -->
<footer>
<div class="container">
<a href="#" class="footer-logo">
<span class="logo-dot"></span>Чепухаграм
</a>
<div class="footer-links">
<a href="#features">Функции</a>
<a href="./about-secutity/index.html">Безопасность мессенджера</a>
</div>
<p class="footer-text">© 2026 Чепухаграм. Все права защищены. Разработано с фокусом на абсолютную приватность.</p>
</div>
</footer>
<!-- Interactive script for Chat Mockup Mock animations -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const chat = document.getElementById('mockupChat');
const inputField = document.getElementById('mockupInputText');
const conversation = [
{ isMe: true, text: "Файлы и кружочки шифруются поблочно ключом AES-GCM." },
{ isMe: false, text: "Вау! А ключи файлов шифруются на общем X25519 ключе?" },
{ isMe: true, text: "Да, именно так! Полная крипто-защита." },
{ isMe: false, text: "Супер, теперь я спокоен за свои данные!" }
];
let msgIdx = 0;
// Updates phone time in the mockup to match real time
function updateMockupTime() {
const timeEl = document.querySelector('.mockup-time');
if (timeEl) {
const now = new Date();
timeEl.textContent = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
}
}
updateMockupTime();
setInterval(updateMockupTime, 60000);
function simulateTypingAndSending() {
if (msgIdx >= conversation.length) {
msgIdx = 0;
chat.innerHTML = `
<div class="mockup-bubble partner">
Привет! Ты проверил систему шифрования Чепухаграм?
<span class="time">22:04</span>
</div>
<div class="mockup-bubble me">
Да! Используется X25519 для обмена ключами и AES-256-GCM для шифрования.
<span class="time">22:05</span>
</div>
<div class="mockup-bubble partner">
Отлично! А что с файлами и звонками?
<span class="time">22:05</span>
</div>
`;
setTimeout(simulateTypingAndSending, 3000);
return;
}
const nextMsg = conversation[msgIdx];
// Show typing indicator
const typing = document.createElement('div');
typing.className = 'typing-indicator';
typing.innerHTML = '<span></span><span></span><span></span>';
chat.appendChild(typing);
chat.scrollTop = chat.scrollHeight;
inputField.textContent = "Печатает...";
setTimeout(() => {
// Remove typing indicator
typing.remove();
inputField.textContent = nextMsg.text;
setTimeout(() => {
const bubble = document.createElement('div');
bubble.className = `mockup-bubble ${nextMsg.isMe ? 'me' : 'partner'}`;
const time = new Date();
const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
bubble.innerHTML = `${nextMsg.text}<span class="time">${timeStr}</span>`;
chat.appendChild(bubble);
chat.scrollTop = chat.scrollHeight;
inputField.textContent = "Напишите сообщение...";
msgIdx++;
setTimeout(simulateTypingAndSending, 3500);
}, 800);
}, 2000);
}
setTimeout(simulateTypingAndSending, 3000);
});
</script>
</body>
</html>