+
+ ШИФРОВАНИЕ И ЗАГРУЗКА...
+ 0%
-
-
Секрет готов!
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
СЕКРЕТ ГОТОВ
-
-
+
+
+
+
-
- SCAN OR CLICK
-
-
+
-
-
-
-
Ссылка работает один раз (или согласно лимиту).
+
+ ССЫЛКА ДЕЙСТВИТЕЛЬНА В ПРЕДЕЛАХ ЛИМИТА ИЛИ ТАЙМЕРА.
+
+
-
@@ -226,54 +202,125 @@
function switchTab(mode) {
currentMode = mode;
- // Сбрасываем активные классы
['file', 'text', 'image'].forEach(t => {
- document.getElementById(`tab-${t}`).className = (mode === t) ? 'nav-link active' : 'nav-link';
+ const btn = document.getElementById(`tab-${t}`);
const pane = document.getElementById(`pane-${t}`);
- if(mode === t) pane.classList.remove('d-none');
- else pane.classList.add('d-none');
+
+ if (mode === t) {
+ btn.classList.add('active-tab');
+ pane.classList.remove('hidden');
+ } else {
+ btn.classList.remove('active-tab');
+ pane.classList.add('hidden');
+ }
});
}
- function copyLink() {
- const copyText = document.getElementById("resultLink");
- copyText.select();
- navigator.clipboard.writeText(copyText.value);
- // Визуальный эффект
- const btn = document.querySelector('#resultArea button');
- const originalText = btn.innerText;
- btn.innerText = "✅";
- setTimeout(() => btn.innerText = originalText, 2000);
+ function toggleQr() {
+ document.getElementById('qrCollapse').classList.toggle('hidden');
}
function togglePassword(chk) {
const block = document.getElementById('passwordBlock');
const passInput = document.getElementById('senderPassword');
-
if (chk.checked) {
- block.classList.remove('d-none');
- // Небольшая задержка для плавности анимации (если есть) и фокус
+ block.classList.remove('hidden');
setTimeout(() => passInput.focus(), 50);
} else {
- block.classList.add('d-none');
- passInput.value = ''; // Очищаем пароль, если выключили галочку
+ block.classList.add('hidden');
+ passInput.value = '';
}
}
+ function copyLink() {
+ const copyText = document.getElementById("resultLink");
+ copyText.select();
+ navigator.clipboard.writeText(copyText.value);
+ const btn = document.getElementById('btnCopy');
+ const originalHtml = btn.innerHTML;
+ btn.innerHTML = '
';
+ btn.classList.add('text-emerald-400', 'border-emerald-400');
+ setTimeout(() => {
+ btn.innerHTML = originalHtml;
+ btn.classList.remove('text-emerald-400', 'border-emerald-400');
+ }, 2000);
+ }
+
+ function updateFileName(input) {
+ const display = document.getElementById('fileNameDisplay');
+ if (input.files && input.files[0]) {
+ display.innerText = "ВЫБРАН: " + input.files[0].name;
+ display.classList.add('text-emerald-400', 'font-bold');
+ } else {
+ display.innerText = "МАКС. РАЗМЕР: 5 ГБ";
+ display.classList.remove('text-emerald-400', 'font-bold');
+ }
+ }
+
+ function handleImageSelect(input) {
+ if (input.files && input.files[0]) {
+ processImageFile(input.files[0]);
+ }
+ }
+
+ function processImageFile(file) {
+ if (!file.type.startsWith('image/')) {
+ alert("Это не картинка!");
+ return;
+ }
+ selectedImageBlob = file;
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ document.getElementById('imagePreviewPlaceholder').classList.add('hidden');
+ const img = document.getElementById('imagePreview');
+ img.src = e.target.result;
+ img.classList.remove('hidden');
+ };
+ reader.readAsDataURL(file);
+ }
+
+ document.addEventListener('paste', function(e) {
+ if (currentMode !== 'image') return;
+ const items = (e.clipboardData || e.originalEvent.clipboardData).items;
+ for (let index in items) {
+ const item = items[index];
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
+ const blob = item.getAsFile();
+ processImageFile(blob);
+ e.preventDefault();
+ break;
+ }
+ }
+ });
+
+ // Drag & Drop styling
+ const dropZone = document.getElementById('dropZone');
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
+ dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }, false);
+ });
+ dropZone.addEventListener('dragover', () => dropZone.classList.add('dragover'));
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
+ dropZone.addEventListener('drop', (e) => {
+ dropZone.classList.remove('dragover');
+ const dt = e.dataTransfer;
+ document.getElementById('fileInput').files = dt.files;
+ updateFileName(document.getElementById('fileInput'));
+ });
+
+ // --- Main Logic ---
async function executeActualUpload() {
+
const btn = document.getElementById('btnSend');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const progressArea = document.getElementById('progressArea');
-
- document.getElementById('resultArea').classList.add('d-none');
+ document.getElementById('resultArea').classList.add('hidden');
let blobToSend = null;
let filename = 'secret';
let type = 'file';
try {
- // 1. Подготовка (Файл или Текст)
if (currentMode === 'file') {
const fileInput = document.getElementById('fileInput');
if (!fileInput.files[0]) { alert("Выберите файл!"); return; }
@@ -282,9 +329,9 @@
type = 'file';
}
else if (currentMode === 'image') {
- if (!selectedImageBlob) { alert("Выберите или вставьте картинку!"); return; }
+ if (!selectedImageBlob) { alert("Нет изображения для отправки!"); return; }
blobToSend = selectedImageBlob;
- filename = selectedImageBlob.name || 'image.png'; // Если из буфера, имя может быть пустым
+ filename = selectedImageBlob.name || 'image.png';
type = 'image';
}
else {
@@ -295,78 +342,39 @@
type = 'message';
}
- // Проверка пароля
const usePass = document.getElementById('usePassword').checked;
const password = document.getElementById('senderPassword').value;
if (usePass && !password) { alert("Введите пароль!"); return; }
- // UI
btn.disabled = true;
- btn.innerHTML = '
Шифрование...';
- progressArea.classList.remove('d-none');
+ btn.innerHTML = '
/ ШИФРОВАНИЕ...';
+ progressArea.classList.remove('hidden');
updateProgress(0);
- // 2. Генерация ГЛАВНОГО ФАЙЛОВОГО КЛЮЧА (Master Key)
- const fileKey = await window.crypto.subtle.generateKey(
- { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]
- );
+ const fileKey = await window.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const fileIvBase = window.crypto.getRandomValues(new Uint8Array(12));
-
- // Переменные для сервера и ссылки
- let urlHashKey = null; // Ключ, который пойдет в ссылку (#...)
- let passwordSalt = null; // Соль для сервера
- let wrappedKeyBlob = null; // Зашифрованный ключ для сервера
+ let urlHashKey = null; let passwordSalt = null; let wrappedKeyBlob = null;
if (usePass) {
- // === ДВОЙНОЕ ШИФРОВАНИЕ (МАТРЕШКА) ===
-
- // A. Генерируем случайный ключ для Ссылки (URL Key)
- const urlKey = await window.crypto.subtle.generateKey(
- { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]
- );
-
- // B. Генерируем ключ из Пароля (Password Key)
+ const urlKey = await window.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const salt = window.crypto.getRandomValues(new Uint8Array(16));
passwordSalt = toBase64(salt);
const passKey = await deriveKeyFromPassword(password, salt);
-
- // C. Экспортируем Master Key в байты
const rawFileKey = await window.crypto.subtle.exportKey("raw", fileKey);
-
- // D. СЛОЙ 1: Шифруем Master Key Паролем
const innerIv = window.crypto.getRandomValues(new Uint8Array(12));
- const innerEncrypted = await window.crypto.subtle.encrypt(
- { name: "AES-GCM", iv: innerIv }, passKey, rawFileKey
- );
-
- // E. СЛОЙ 2: Шифруем результат Ключом из Ссылки
+ const innerEncrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: innerIv }, passKey, rawFileKey);
const outerIv = window.crypto.getRandomValues(new Uint8Array(12));
- // Важно: шифруем (IV1 + Cipher1)
const innerBlob = new Uint8Array(innerIv.length + innerEncrypted.byteLength);
- innerBlob.set(innerIv);
- innerBlob.set(new Uint8Array(innerEncrypted), 12);
-
- const outerEncrypted = await window.crypto.subtle.encrypt(
- { name: "AES-GCM", iv: outerIv }, urlKey, innerBlob
- );
-
- // F. Собираем финальную матрешку: [OuterIV] + [Encrypted( InnerIV + EncryptedMasterKey )]
+ innerBlob.set(innerIv); innerBlob.set(new Uint8Array(innerEncrypted), 12);
+ const outerEncrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: outerIv }, urlKey, innerBlob);
const finalBlob = new Uint8Array(outerIv.length + outerEncrypted.byteLength);
- finalBlob.set(outerIv);
- finalBlob.set(new Uint8Array(outerEncrypted), 12);
-
+ finalBlob.set(outerIv); finalBlob.set(new Uint8Array(outerEncrypted), 12);
wrappedKeyBlob = toBase64(finalBlob);
-
- // G. В ссылку кладем URL Key
const exportedUrlKey = await window.crypto.subtle.exportKey("jwk", urlKey);
urlHashKey = exportedUrlKey.k;
-
} else {
- // === ОБЫЧНОЕ ШИФРОВАНИЕ (БЕЗ ПАРОЛЯ) ===
- // В ссылку кладем сам Master Key
const jwk = await window.crypto.subtle.exportKey("jwk", fileKey);
urlHashKey = jwk.k;
- // wrappedKeyBlob остается null
}
// Получаем токен из виджета
@@ -400,43 +408,34 @@
const { id } = await initResp.json();
- // 4. Шифрование и загрузка файла (Chunked)
let offset = 0;
let chunkIndex = 0;
while (offset < blobToSend.size) {
const chunkBlob = blobToSend.slice(offset, offset + CHUNK_SIZE);
const chunkBuffer = await chunkBlob.arrayBuffer();
-
const chunkIv = deriveIv(fileIvBase, chunkIndex);
- const encryptedChunk = await window.crypto.subtle.encrypt(
- { name: "AES-GCM", iv: chunkIv }, fileKey, chunkBuffer
- );
-
- const chunkResp = await fetch(`?handler=Chunk&id=${id}`, {
- method: 'POST', body: encryptedChunk
- });
- if (!chunkResp.ok) throw new Error("Upload Error");
-
+ const encryptedChunk = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv: chunkIv }, fileKey, chunkBuffer);
+ const chunkResp = await fetch(`?handler=Chunk&id=${id}`, { method: 'POST', body: encryptedChunk });
+ if (!chunkResp.ok) throw new Error("ОШИБКА ЗАГРУЗКИ ЧАСТИ");
offset += CHUNK_SIZE;
chunkIndex++;
updateProgress(Math.min(100, Math.round((offset / blobToSend.size) * 100)));
}
- // 5. Результат
const link = `${window.location.origin}/view/${id}#${urlHashKey}`;
document.getElementById('resultLink').value = link;
- document.getElementById('resultArea').classList.remove('d-none');
+ document.getElementById('resultArea').classList.remove('hidden');
if (typeof generateQR === "function") generateQR(link);
} catch (err) {
console.error(err);
- alert("Ошибка: " + err.message);
- if(progressBar) progressBar.classList.add('bg-danger');
+ alert("ОШИБКА: " + err.message);
+ if(progressBar) progressBar.classList.add('bg-red-500');
} finally {
btn.disabled = false;
- btn.innerHTML = '
Зашифровать и получить ссылку';
- progressArea.classList.add('d-none');
- if(progressBar) progressBar.classList.remove('bg-danger');
+ btn.innerHTML = '
ЗАШИФРОВАТЬ И ОТПРАВИТЬ';
+ progressArea.classList.add('hidden');
+ if(progressBar) progressBar.classList.remove('bg-red-500');
}
}
@@ -485,36 +484,14 @@
if(txt) txt.innerText = percent + '%';
}
- function downloadQrImage() {
- const container = document.getElementById("qrcode");
- // Библиотека qrcodejs создает либо canvas, либо img (в старых браузерах).
- // Чаще всего это canvas.
- const canvas = container.querySelector("canvas");
- const img = container.querySelector("img");
-
- let url = null;
-
- if (canvas) {
- // Конвертируем Canvas в DataURL (base64 картинка)
- url = canvas.toDataURL("image/png");
- } else if (img) {
- // Если вдруг библиотека отрендерила IMG тег
- url = img.src;
- } else {
- alert("QR-код еще не сгенерирован.");
- return;
- }
-
- // Создаем временную ссылку для скачивания
- const link = document.createElement("a");
- link.href = url;
- link.download = "secret-qr.png"; // Имя файла при скачивании
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ async function deriveKeyFromPassword(password, salt) {
+ const enc = new TextEncoder();
+ const keyMaterial = await window.crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]);
+ return await window.crypto.subtle.deriveKey(
+ { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" },
+ keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
+ );
}
-
- // ... toBase64 и deriveIv остаются без изменений ...
function deriveIv(baseIv, counter) {
const iv = new Uint8Array(baseIv);
const view = new DataView(iv.buffer);
@@ -523,187 +500,25 @@
return iv;
}
function toBase64(u8) { return btoa(String.fromCharCode.apply(null, u8)); }
-
- // Добавим маленькую функцию для красоты имени файла
- function updateFileName(input) {
- const display = document.getElementById('fileNameDisplay');
- if (input.files && input.files[0]) {
- display.innerText = "Выбран: " + input.files[0].name;
- display.classList.add('text-primary', 'fw-bold');
- } else {
- display.innerText = "Поддерживаются файлы любого размера";
- display.classList.remove('text-primary', 'fw-bold');
- }
- }
-
function generateQR(text) {
const container = document.getElementById("qrcode");
- container.innerHTML = ""; // Очищаем, если там что-то было
-
+ container.innerHTML = "";
new QRCode(container, {
- text: text,
- width: 160,
- height: 160,
- colorDark : "#000000",
- colorLight : "#ffffff",
+ text: text, width: 128, height: 128,
+ colorDark : "#000000", colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
});
}
-
- // Drag & Drop Visuals
- const dropZone = document.getElementById('dropZone');
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
- dropZone.addEventListener(eventName, preventDefaults, false);
- });
- function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
-
- dropZone.addEventListener('dragover', () => dropZone.classList.add('dragover'));
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
- dropZone.addEventListener('drop', (e) => {
- dropZone.classList.remove('dragover');
- const dt = e.dataTransfer;
- document.getElementById('fileInput').files = dt.files;
- updateFileName(document.getElementById('fileInput'));
- });
-
- // ... Вставь функцию copyLink, но обнови ID кнопки ...
- function copyLink() {
- const copyText = document.getElementById("resultLink");
- copyText.select();
- navigator.clipboard.writeText(copyText.value);
-
- const btn = document.getElementById('btnCopy');
- const icon = btn.querySelector('i');
- icon.classList.remove('bi-clipboard');
- icon.classList.add('bi-check-lg');
- btn.classList.add('btn-success');
- btn.classList.remove('btn-outline-success');
-
- setTimeout(() => {
- icon.classList.add('bi-clipboard');
- icon.classList.remove('bi-check-lg');
- btn.classList.remove('btn-success');
- btn.classList.add('btn-outline-success');
- }, 2000);
- }
- async function copyQrImage() {
+ function copyQrImage() {
const container = document.getElementById("qrcode");
- // Библиотека генерирует canvas. Найдем его.
const canvas = container.querySelector("canvas");
-
- if (!canvas) {
- alert("Ошибка: QR-код еще не сгенерирован или браузер не поддерживает Canvas.");
- return;
- }
-
- try {
- // 1. Конвертируем Canvas в Blob (PNG)
- canvas.toBlob(async function(blob) {
- if (!blob) return;
-
- // 2. Создаем элемент буфера обмена
- // Внимание: Clipboard API требует HTTPS (или localhost)
+ if (!canvas) return;
+ canvas.toBlob(async function(blob) {
+ try {
const item = new ClipboardItem({ "image/png": blob });
-
- // 3. Пишем в буфер
await navigator.clipboard.write([item]);
-
- // 4. Показываем эффект успеха
- showQrCopiedEffect();
- }, 'image/png');
-
- } catch (err) {
- console.error("Не удалось скопировать QR:", err);
- alert("Не удалось скопировать. Возможно, нет контекста безопасности (HTTPS).");
- }
- }
-
- function showQrCopiedEffect() {
- const badge = document.getElementById('qrCopiedBadge');
- const container = document.getElementById('qrcode-container');
-
- // Показываем плашку
- badge.classList.remove('d-none');
-
- // Легкий визуальный "отклик" контейнера
- container.style.transform = "scale(0.95)";
- setTimeout(() => container.style.transform = "scale(1)", 150);
-
- // Прячем через 2 секунды
- setTimeout(() => {
- badge.classList.add('d-none');
- }, 2000);
- }
-
- async function deriveKeyFromPassword(password, salt) {
- const enc = new TextEncoder();
- const keyMaterial = await window.crypto.subtle.importKey(
- "raw",
- enc.encode(password),
- { name: "PBKDF2" },
- false,
- ["deriveKey"]
- );
-
- return await window.crypto.subtle.deriveKey(
- {
- name: "PBKDF2",
- salt: salt,
- iterations: 100000, // 100k итераций - хороший баланс скорости и защиты
- hash: "SHA-256"
- },
- keyMaterial,
- { name: "AES-GCM", length: 256 }, // На выходе хотим AES ключ
- false,
- ["encrypt", "decrypt"]
- );
- }
-
- function updateProgress(percent) {
- const bar = document.getElementById('progressBar');
- const txt = document.getElementById('progressText');
- if(bar) bar.style.width = percent + '%';
- if(txt) txt.innerText = percent + '%';
- }
-
- function handleImageSelect(input) {
- if (input.files && input.files[0]) {
- processImageFile(input.files[0]);
- }
- }
-
- // Общая функция обработки файла картинки (для Input и Paste)
- function processImageFile(file) {
- if (!file.type.startsWith('image/')) {
- alert("Это не картинка!");
- return;
- }
- selectedImageBlob = file;
-
- // Показываем превью
- const reader = new FileReader();
- reader.onload = function(e) {
- document.getElementById('imagePreviewPlaceholder').classList.add('d-none');
- const img = document.getElementById('imagePreview');
- img.src = e.target.result;
- img.classList.remove('d-none');
- };
- reader.readAsDataURL(file);
+ alert("QR-КОД СКОПИРОВАН");
+ } catch (e) { alert("ОШИБКА КОПИРОВАНИЯ (НУЖЕН HTTPS)"); }
+ });
}
-
- // Обработчик PASTE (Ctrl+V)
- document.addEventListener('paste', function(e) {
- if (currentMode !== 'image') return;
-
- const items = (e.clipboardData || e.originalEvent.clipboardData).items;
- for (let index in items) {
- const item = items[index];
- if (item.kind === 'file' && item.type.startsWith('image/')) {
- const blob = item.getAsFile();
- processImageFile(blob);
- e.preventDefault(); // Чтобы не вставилось в другие поля
- break;
- }
- }
- });
\ No newline at end of file
diff --git a/SecretDrop/Pages/Request.cshtml b/SecretDrop/Pages/Request.cshtml
index f163e18..1337e56 100644
--- a/SecretDrop/Pages/Request.cshtml
+++ b/SecretDrop/Pages/Request.cshtml
@@ -1,166 +1,164 @@
@page "{id?}"
@model RequestModel
@{
- ViewData["Title"] = Model.IsSenderMode ? "Отправка файла" : "Запрос файла";
- // Получаем SiteKey из конфига (предположим, ты передал его через ViewData или Model)
- // Если нет, просто замени строку ниже на свой ключ '0x4AAAAAA...'
- var siteKey = "ТВОЙ_SITE_KEY_ОТ_CLOUDFLARE";
+ ViewData["Title"] = Model.IsSenderMode ? "Безопасный канал" : "Шлюз";
}
-
-
-
-
-
-
+
@if (!Model.IsSenderMode)
{
-
-
-
-
-
+
+
+
+
-
Прием файлов
-
- Сгенерируйте одноразовую ссылку-шлюз. Отправьте её собеседнику,
- и он сможет передать вам файл, текст или фото через защищенный RSA-туннель.
+
ПРИЕМ ФАЙЛОВ
+
+ Создайте одноразовый защищенный шлюз (RSA-2048). Отправьте ссылку собеседнику, чтобы он мог передать вам данные через шифрованный канал.
-
-
-
-
-
Ожидание подключения...
+
+
+
+
ОЖИДАНИЕ ПОДКЛЮЧЕНИЯ...
-