This commit is contained in:
Kirchik
2025-08-13 22:36:08 +02:00
parent 6d061248de
commit e23fc02f63
254 changed files with 9579 additions and 4378 deletions
+271
View File
@@ -0,0 +1,271 @@
/**
* Client-side файловое шифрование для форм
* Шифрует файлы в браузере пользователя перед отправкой
*/
class FileEncryption {
constructor() {
this.masterKey = 'sleeptrip_secure_key_2025'; // В продакшне должен быть более сложный
this.encryptedFiles = new Map(); // Хранилище зашифрованных файлов
}
/**
* Генерация криптографического ключа из мастер-пароля
*/
async generateKey(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{
name: 'AES-GCM',
length: 256
},
false,
['encrypt', 'decrypt']
);
}
/**
* Шифрование файла
*/
async encryptFile(file) {
try {
// Генерируем соль и IV
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
// Создаем ключ
const key = await this.generateKey(this.masterKey, salt);
// Читаем файл
const fileBuffer = await file.arrayBuffer();
// Шифруем
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
fileBuffer
);
// Создаем метаданные
const metadata = {
fileName: file.name,
fileType: file.type,
fileSize: file.size,
encryptedAt: new Date().toISOString(),
salt: Array.from(salt),
iv: Array.from(iv)
};
// Комбинируем метаданные и зашифрованные данные
const metadataStr = JSON.stringify(metadata);
const metadataBytes = new TextEncoder().encode(metadataStr);
const metadataLength = new Uint32Array([metadataBytes.length]);
const result = new Uint8Array(
4 + metadataBytes.length + encryptedData.byteLength
);
result.set(new Uint8Array(metadataLength.buffer), 0);
result.set(metadataBytes, 4);
result.set(new Uint8Array(encryptedData), 4 + metadataBytes.length);
return {
encryptedData: result,
metadata: metadata,
originalName: file.name
};
} catch (error) {
console.error('Ошибка шифрования:', error);
throw error;
}
}
/**
* Инициализация шифрования для input элемента
*/
initFileInput(inputId) {
const input = document.getElementById(inputId);
const wrapper = input.closest('.file-input-wrapper');
const textElement = wrapper.querySelector('.file-input-text');
// Создаем контейнер для статуса шифрования
let statusDiv = wrapper.parentElement.querySelector('.encryption-status');
if (!statusDiv) {
statusDiv = document.createElement('div');
statusDiv.className = 'encryption-status';
wrapper.parentElement.appendChild(statusDiv);
}
input.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
try {
// Показываем процесс шифрования
this.showEncryptionStatus(statusDiv, 'encrypting', 'Шифрование файла...');
textElement.textContent = `Шифрую: ${file.name}`;
textElement.className = 'file-input-text file-selected';
// Ждем немного для показа анимации
await new Promise(resolve => setTimeout(resolve, 500));
// Шифруем файл
const encryptedFile = await this.encryptFile(file);
// Сохраняем зашифрованный файл
this.encryptedFiles.set(inputId, encryptedFile);
// Обновляем UI
textElement.textContent = `${encryptedFile.originalName} (зашифрован)`;
textElement.className = 'file-input-text file-encrypted';
wrapper.classList.add('file-encrypted');
this.showEncryptionStatus(statusDiv, 'encrypted', '✅ Файл зашифрован и готов к отправке');
// Создаем скрытое поле с зашифрованными данными
this.createHiddenEncryptedField(input.form, inputId, encryptedFile);
} catch (error) {
console.error('Ошибка при шифровании файла:', error);
this.showEncryptionStatus(statusDiv, 'error', '❌ Ошибка шифрования файла');
textElement.textContent = 'Выберите файл';
textElement.className = 'file-input-text';
wrapper.classList.remove('file-encrypted');
}
});
}
/**
* Показ статуса шифрования
*/
showEncryptionStatus(statusDiv, type, message) {
statusDiv.className = `encryption-status ${type}`;
if (type === 'encrypting') {
statusDiv.innerHTML = `
<span class="encrypting-spinner"></span>
${message}
`;
} else {
statusDiv.textContent = message;
}
}
/**
* Создание скрытого поля с зашифрованными данными
*/
createHiddenEncryptedField(form, inputId, encryptedFile) {
// Удаляем предыдущее скрытое поле если есть
const existingField = form.querySelector(`input[name="${inputId}_encrypted"]`);
if (existingField) {
existingField.remove();
}
// Создаем новое скрытое поле
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = `${inputId}_encrypted`;
// Конвертируем зашифрованные данные в base64
const base64Data = btoa(String.fromCharCode(...encryptedFile.encryptedData));
hiddenField.value = base64Data;
form.appendChild(hiddenField);
// Также создаем поле с метаданными
const metadataField = document.createElement('input');
metadataField.type = 'hidden';
metadataField.name = `${inputId}_metadata`;
metadataField.value = JSON.stringify(encryptedFile.metadata);
form.appendChild(metadataField);
}
/**
* Получение зашифрованного файла по ID
*/
getEncryptedFile(inputId) {
return this.encryptedFiles.get(inputId);
}
/**
* Проверка наличия зашифрованных файлов перед отправкой формы
*/
validateEncryptedFiles(form) {
const fileInputs = form.querySelectorAll('input[type="file"]');
const encryptedFields = form.querySelectorAll('input[name$="_encrypted"]');
for (let input of fileInputs) {
if (input.files.length > 0) {
const encryptedField = form.querySelector(`input[name="${input.id}_encrypted"]`);
if (!encryptedField || !encryptedField.value) {
alert('Не все файлы зашифрованы. Пожалуйста, дождитесь завершения шифрования.');
return false;
}
}
}
return true;
}
}
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
console.log('🔐 Инициализация системы шифрования...');
// Проверяем поддержку WebCrypto
if (!window.crypto || !window.crypto.subtle) {
console.error('❌ WebCrypto API не поддерживается в этом браузере');
alert('Ваш браузер не поддерживает шифрование. Используйте современный браузер.');
return;
}
const encryption = new FileEncryption();
// Инициализируем все file input элементы
const fileInputs = document.querySelectorAll('input[type="file"]');
console.log(`📂 Найдено ${fileInputs.length} файловых input элементов`);
fileInputs.forEach((input, index) => {
if (input.id) {
console.log(`🔧 Инициализируем шифрование для: ${input.id}`);
encryption.initFileInput(input.id);
} else {
console.warn(`⚠️ File input ${index} не имеет ID, пропускаем`);
}
});
// Добавляем валидацию к формам перед отправкой
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(event) {
console.log('📤 Отправка формы, проверяем шифрование...');
if (!encryption.validateEncryptedFiles(form)) {
event.preventDefault();
return false;
}
});
});
// Делаем encryption доступным глобально для отладки
window.fileEncryption = encryption;
console.log('✅ Система шифрования инициализирована');
});