Анонимная отправка зашифрованного сообщения
Как это работает:
Шифрование выполняется локально в вашем браузере.
Сервер получает только зашифрованный набор байт, перехват которого бесполезен без приватного ключа.
Дешифровка сообщения возможна только владельцем приватного ключа.
Приватный ключ существует только в оффлайне.
Но ваш провайдер (а значит и государство) видит то, что вы зашли на этот сайт (факт установки соединения).
Поэтому для максимальной защиты от DPI и провайдера обязательно используйте для отправки Tor Browser.
Но ваш провайдер (а значит и государство) видит то, что вы зашли на этот сайт (факт установки соединения).
Поэтому для максимальной защиты от 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>