Анонимная отправка зашифрованного сообщения

Как это работает:

Шифрование выполняется локально в вашем браузере. Сервер получает только зашифрованный набор байт, перехват которого бесполезен без приватного ключа. Дешифровка сообщения возможна только владельцем приватного ключа. Приватный ключ существует только в оффлайне.

Но ваш провайдер (а значит и государство) видит то, что вы зашли на этот сайт (факт установки соединения).

Поэтому для максимальной защиты от DPI и провайдера обязательно используйте для отправки Tor Browser.

Рекурсивный аудит исходного кода

Убедитесь, что нет скрытых полей или отправки иных данных кроме зашифрованного текста.

<?php
/**
 * Этот файл является самодостаточным. Он шифрует сообщение на стороне клиента
 * с использованием Web Crypto API и публичного RSA-ключа, после чего безопасно передаёт шифр на сервер.
 *
 * @author          Uladzimir Zhylinski <[email protected]>
 * @security-review Claude Sonnet 4.6 (Anthropic), 2026-05-04
 */

// ==========================================
// 1. КОНФИГУРАЦИЯ
// ==========================================

// Максимальный размер входящего запроса (64 КБ достаточно для RSA-2048 + AES-GCM payload)
define('MAX_INPUT_BYTES', 65536);

// Имя каталога и файла хранилища вычисляется во время выполнения из константы-«соли»,
// чтобы пути не присутствовали в исходном коде явными читаемыми строками.
$storageDir  = __DIR__ . '/' . substr(hash('sha256', 'secure-dropbox-dir-v1'), 0, 16);
$storageFile = $storageDir  . '/' . substr(hash('sha256', 'secure-dropbox-v1'),     0, 24) . '.dat';

// Создаём каталог, если ещё не существует
if (!is_dir($storageDir)) {
    mkdir($storageDir, 0750, true);
}

// ПУБЛИЧНЫЙ КЛЮЧ RSA (в формате PEM)
$rsaPublicKeyPem = <<<EOD
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyx5ELCAEAg26Fud3EC4p
yFJdNbhClh/Ku1hGXiu4OD+zZeHmrm3Q/4P0KsC6TGP/UQcjl+iyWwS1mKpFSX1y
NpAJuOHDERRos9XY5qzL3NU0VMY1VXOWUjdWaIS/JU/K8tqOB9S7UfdGWfsjEd8q
C4VxBJnOmA6b2ErJxkZDisx3QxK8z6C2pnS7539yWtvMkqMPDkpq0fohMVHP944+
B+KiCW+x4DAk/fvWZyqKU0yvPiWbRZCcLj+MCaSNvtArmHfBeg5YGoVoMnCjjl+g
r1thygLrAU/bw2vWIFYO6601+6niMc+5UzTC46qVq5KHmBsHxS5uP6syW2/04zZe
ONhX8+D/38EPkTrQrBaq4ee/khdLWX4oQnoDPL3X+KD7nrcdS4qswzlDFy3Mo4TR
Z9T7u5ZPspZukLZB8uVJyWmdZ52y2/g+LUvnaIgix7CHP6FZtbUMN93fh/PK3OUj
7EnBP/f06ucWADgr8jUYLyBTOFKwUIFcuDwczC427l5w+YdC5KV2a6LsQ3dUyyUk
5PqsPX0cxp98eoIzRAuMFFhwpnn28CF4GVKIUExk8gWFNi37iPqOyp56oVKKETgM
ucB1kRE2umKHqQM+lYlJnyv3MKOsWAtv/lhYNvmVZwqJ7mJviLzfcLlWqSCPQfAQ
nkd5nyKXzB31p2AjhUTTf88CAwEAAQ==
-----END PUBLIC KEY-----
EOD;

// ==========================================
// 2. ОБРАБОТКА ШИФРОВАННОГО ЗАПРОСА
// ==========================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Устанавливаем заголовки для предотвращения кэширования
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Content-Type: application/json');

    // Ограничение размера входящего запроса (защита от DoS и переполнения диска)
    $inputData = file_get_contents('php://input', false, null, 0, MAX_INPUT_BYTES + 1);

    if ($inputData === false || strlen($inputData) === 0) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'message' => 'Empty request body']);
        exit;
    }

    if (strlen($inputData) > MAX_INPUT_BYTES) {
        http_response_code(413);
        echo json_encode(['status' => 'error', 'message' => 'Payload too large']);
        exit;
    }

    $json = json_decode($inputData, true);

    if (!is_array($json) || !isset($json['encryptedPayload']) || !is_string($json['encryptedPayload'])) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'message' => 'Invalid payload']);
        exit;
    }

    // Валидация формата: payload должен быть строгим Base64
    // (RSA-зашифрованный AES-ключ + IV + шифртекст, упакованные btoa(JSON))
    if (!preg_match('/^[A-Za-z0-9+\/=]+$/', $json['encryptedPayload'])) {
        http_response_code(400);
        echo json_encode(['status' => 'error', 'message' => 'Invalid payload format']);
        exit;
    }

    // Сохраняем только сырой зашифрованный payload, время и разделитель.
    // Никакие заголовки (User-Agent, IP) здесь не логируются намеренно.
    $entry = "[" . date('Y-m-d H:i:s') . "]\n" . $json['encryptedPayload'] . "\n" . str_repeat("-", 40) . "\n";

    if (file_put_contents($storageFile, $entry, FILE_APPEND | LOCK_EX) !== false) {
        echo json_encode(['status' => 'success']);
    } else {
        http_response_code(500);
        echo json_encode(['status' => 'error', 'message' => 'Internal storage error']);
    }
    exit;
}

// ==========================================
// 3. ВЫВОД СТРАНИЦЫ
// ==========================================
// Генерируем nonce для Content-Security-Policy (inline-скрипт)
$cspNonce = base64_encode(random_bytes(16));

header("Content-Security-Policy: default-src 'none'; script-src 'nonce-{$cspNonce}'; style-src 'self' 'unsafe-inline'; connect-src 'self'; form-action 'self'; font-src 'self' https://fonts.gstatic.com; img-src 'self'");
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
?>
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Анонимная отправка</title>
    <link rel="stylesheet" href="/styles.css?v=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/styles.css') ?>">
    <style>
        textarea#messageBox {
            min-height: 160px;
            font-family: inherit;
        }
        #sendBtn {
            margin-top: 12px;
        }
        #status {
            margin-top: 12px;
            padding: 12px 16px;
            border-radius: 8px;
            font-size: 14px;
            display: none;
        }
        #status.success { background: #e6f4ea; color: #1e7e34; }
        #status.error   { background: #fde8e8; color: #c0392b; }
        .warning-box {
            background: #fff8e1;
            border-left: 4px solid #f59e0b;
            padding: 12px 16px;
            border-radius: 0 8px 8px 0;
            margin-bottom: 20px;
            font-size: 14px;
            color: #78350f;
        }
        .code-container {
            background: #f7f7f8;
            border: 1.5px solid #e5e7eb;
            border-radius: 8px;
            padding: 16px;
            overflow-x: auto;
            margin-top: 32px;
            font-size: 13px;
        }
    </style>
</head>
<body>
<main class="wrap" role="main">

    <header>
        <div>
            <h1>Анонимная отправка зашифрованного сообщения</h1>
            <p class="lead">Как это работает:</p>
        </div>
    </header>

    <div class="warning-box">
        Шифрование выполняется локально в вашем браузере.
        Сервер получает только зашифрованный набор байт, перехват которого бесполезен без приватного ключа.
        Дешифровка сообщения возможна только владельцем приватного ключа.
        Приватный ключ существует только в оффлайне.
        <br><br>
        Но ваш провайдер (а значит и государство) видит то, что вы зашли на этот сайт (факт установки соединения).
        <br><br>
        <em>Поэтому для максимальной защиты от DPI и провайдера обязательно используйте для отправки <a href="https://www.torproject.org/download/">Tor Browser</a>.</em>
    </div>

    <div class="form-grid">
        <div class="form-group wide">
            <label for="messageBox">Ваше сообщение</label>
            <textarea id="messageBox" placeholder="Введите текст сообщения здесь"></textarea>
        </div>
    </div>
    <div class="form-actions">
        <button id="sendBtn" class="btn">Зашифровать и отправить</button>
    </div>

    <div id="status"></div>

    <script nonce="<?php echo $cspNonce; ?>">
        // Публичный ключ из PHP передаётся в JS
        const publicKeyPem = `<?php echo $rsaPublicKeyPem; ?>`;

        // Конвертация PEM в ArrayBuffer
        function pemToArrayBuffer(pem) {
            const b64Lines = pem
                .replace('-----BEGIN PUBLIC KEY-----', '')
                .replace('-----END PUBLIC KEY-----', '')
                .replace(/\s+/g, '');
            const binary = window.atob(b64Lines);
            const buf = new ArrayBuffer(binary.length);
            const bufView = new Uint8Array(buf);
            for (let i = 0; i < binary.length; i++) {
                bufView[i] = binary.charCodeAt(i);
            }
            return buf;
        }

        // Кодирование ArrayBuffer в Base64
        function arrayBufferToBase64(buffer) {
            let binary = '';
            const bytes = new Uint8Array(buffer);
            for (let i = 0; i < bytes.byteLength; i++) {
                binary += String.fromCharCode(bytes[i]);
            }
            return window.btoa(binary);
        }

        async function encryptAndSend() {
            const msgEl   = document.getElementById('messageBox');
            const btnEl   = document.getElementById('sendBtn');
            const message = msgEl.value.trim();

            if (!message) {
                showStatus('Сообщение пустое.', false);
                return;
            }

            btnEl.disabled = true;
            btnEl.innerText = 'Шифрование...';

            try {
                // 1. Импорт RSA публичного ключа
                //    extractable: false — ключ нельзя экспортировать из Web Crypto API
                const keyBuffer = pemToArrayBuffer(publicKeyPem);
                const rsaPubKey = await window.crypto.subtle.importKey(
                    'spki',
                    keyBuffer,
                    { name: 'RSA-OAEP', hash: 'SHA-256' },
                    false,
                    ['encrypt']
                );

                // 2. Генерация одноразового AES-GCM ключа
                const aesKey = await window.crypto.subtle.generateKey(
                    { name: 'AES-GCM', length: 256 },
                    true,
                    ['encrypt', 'decrypt']
                );

                // 3. Шифрование сообщения с помощью AES-GCM
                const encodedMessage = new TextEncoder().encode(message);
                const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 96-битный IV

                const encryptedMessage = await window.crypto.subtle.encrypt(
                    { name: 'AES-GCM', iv },
                    aesKey,
                    encodedMessage
                );

                // 4. Экспорт AES-ключа и его шифрование с помощью RSA-OAEP
                const exportedAesKey  = await window.crypto.subtle.exportKey('raw', aesKey);
                const encryptedAesKey = await window.crypto.subtle.encrypt(
                    { name: 'RSA-OAEP' },
                    rsaPubKey,
                    exportedAesKey
                );

                // 5. Упаковка всего в один Base64-payload
                const payloadObj = {
                    aesKey:     arrayBufferToBase64(encryptedAesKey),
                    iv:         arrayBufferToBase64(iv),
                    ciphertext: arrayBufferToBase64(encryptedMessage)
                };

                const finalPayload = window.btoa(JSON.stringify(payloadObj));

                btnEl.innerText = 'Отправка...';

                // 6. Отправка зашифрованного payload на сервер
                const response = await fetch(window.location.href, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ encryptedPayload: finalPayload })
                });

                const result = await response.json();

                if (response.ok && result.status === 'success') {
                    showStatus('Сообщение успешно зашифровано и доставлено!', true);
                    msgEl.value = '';
                } else {
                    throw new Error(result.message || 'Ошибка сервера');
                }

            } catch (error) {
                console.error(error);
                showStatus('Произошла ошибка при шифровании или отправке.', false);
            } finally {
                btnEl.disabled = false;
                btnEl.innerText = 'Зашифровать и отправить';
            }
        }

        function showStatus(text, isSuccess) {
            const statusEl = document.getElementById('status');
            statusEl.innerText = text;
            statusEl.className = isSuccess ? 'success' : 'error';
            statusEl.style.display = 'block';
        }

        document.getElementById('sendBtn').addEventListener('click', encryptAndSend);
    </script>

    <hr style="border-color: #e5e7eb; margin-top: 40px;">
    <h3>Рекурсивный аудит исходного кода</h3>
    <p class="lead" style="margin-top: 4px;">
        Убедитесь, что нет скрытых полей или отправки иных данных кроме зашифрованного текста.
    </p>

    <div class="code-container">
        <?php
        // Вывод исходного кода текущего файла
        ini_set('highlight.comment', '#6b7280');
        ini_set('highlight.default', '#111111');
        ini_set('highlight.html',    '#6b7280');
        ini_set('highlight.keyword', '#0b66c3');
        ini_set('highlight.string',  '#16a34a');

        echo highlight_file(__FILE__, true);

        // P.S.: Специально для товарища майора и прочих роскомпозоров:
        // приватного ключа на этом сервере нет, идите нахуй 8)
        ?>
    </div>

    <footer>
        <div class="mt-8 fs-13">2026 © <strong><a class="link" style="color: gray !important;" href="https://zhylinski.com/">Владимир Жилинский</a></strong>.</div>
    </footer>

</main>
</body>
</html>