wip(encryption): experimental support for encrypted file transfer via chunks

Added an early implementation of secure file transfer using chunk-based encryption.
Files are split into encrypted chunks and transmitted over the chat channel.

This feature is still under active development and requires further changes and testing.
This commit is contained in:
lockbitchat
2025-08-18 21:45:50 -04:00
parent 857d7d74ab
commit dadc80a755
5 changed files with 2580 additions and 462 deletions

View File

@@ -2321,10 +2321,11 @@
keyFingerprint,
isVerified,
chatMessagesRef,
scrollToBottom
scrollToBottom,
webrtcManager
}) => {
const [showScrollButton, setShowScrollButton] = React.useState(false);
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
React.useEffect(() => {
if (chatMessagesRef.current && messages.length > 0) {
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
@@ -2472,7 +2473,28 @@
className: 'fas fa-chevron-down'
})
]),
React.createElement('div', {
key: 'file-section',
className: "file-transfer-section max-w-4xl mx-auto p-4 border-t border-gray-500/10"
}, [
React.createElement('button', {
key: 'toggle-files',
onClick: () => setShowFileTransfer(!showFileTransfer),
className: `flex items-center text-sm text-secondary hover:text-primary transition-colors ${showFileTransfer ? 'mb-4' : ''}`
}, [
React.createElement('i', {
key: 'icon',
className: `fas fa-${showFileTransfer ? 'chevron-up' : 'paperclip'} mr-2`
}),
showFileTransfer ? 'Hide file transfer' : 'Send files'
]),
showFileTransfer && React.createElement(FileTransferComponent, {
key: 'file-transfer',
webrtcManager: webrtcManager,
isConnected: isVerified
})
]),
// Enhanced Chat Input Area
React.createElement('div', {
key: 'chat-input',
@@ -2646,6 +2668,9 @@
}, [sessionManager]);
const webrtcManagerRef = React.useRef(null);
// Expose for modules/UI that run outside this closure (e.g., inline handlers)
// Safe because it's a ref object and we maintain it centrally here
window.webrtcManagerRef = webrtcManagerRef;
const addMessageWithAutoScroll = (message, type) => {
const newMessage = {
@@ -2880,30 +2905,17 @@
updateSecurityLevel().catch(console.error);
}
} else if (status === 'disconnected') {
if (sessionManager && sessionManager.hasActiveSession()) {
sessionManager.resetSession();
setSessionTimeLeft(0);
setHasActiveSession(false);
}
document.dispatchEvent(new CustomEvent('peer-disconnect'));
// Complete UI reset on disconnect
setKeyFingerprint('');
setVerificationCode('');
setSecurityLevel(null);
// При ошибках соединения не сбрасываем сессию полностью
// только обновляем статус соединения
setConnectionStatus('disconnected');
setIsVerified(false);
setShowVerification(false);
setConnectionStatus('disconnected');
setMessages([]);
// Не очищаем консоль и не сбрасываем сообщения
// чтобы пользователь мог видеть ошибки
if (typeof console.clear === 'function') {
console.clear();
}
setTimeout(() => {
setSessionManager(null);
}, 1000);
// Не сбрасываем сессию при ошибках соединения
// только при намеренном отключении
} else if (status === 'peer_disconnected') {
if (sessionManager && sessionManager.hasActiveSession()) {
sessionManager.resetSession();
@@ -2922,11 +2934,12 @@
setShowVerification(false);
setConnectionStatus('disconnected');
setMessages([]);
if (typeof console.clear === 'function') {
console.clear();
}
// Не очищаем сообщения и консоль при отключении пира
// чтобы сохранить историю соединения
// setMessages([]);
// if (typeof console.clear === 'function') {
// console.clear();
// }
setSessionManager(null);
}, 2000);
@@ -3058,6 +3071,43 @@
document.addEventListener('visibilitychange', handleVisibilityChange);
// Setup file transfer callbacks
if (webrtcManagerRef.current) {
webrtcManagerRef.current.setFileTransferCallbacks(
// Progress callback
(progress) => {
console.log('File progress:', progress);
},
// File received callback
(fileData) => {
// Auto-download received file
const url = URL.createObjectURL(fileData.fileBlob);
const a = document.createElement('a');
a.href = url;
a.download = fileData.fileName;
a.click();
URL.revokeObjectURL(url);
addMessageWithAutoScroll(`📥 Файл загружен: ${fileData.fileName}`, 'system');
},
// Error callback
(error) => {
// Более мягкая обработка ошибок файлового трансфера - не закрываем сессию
console.error('File transfer error:', error);
if (error.includes('Connection not ready')) {
addMessageWithAutoScroll(`⚠️ Ошибка передачи файла: соединение не готово. Попробуйте позже.`, 'system');
} else if (error.includes('File too large')) {
addMessageWithAutoScroll(`⚠️ Файл слишком большой. Максимальный размер: 100 МБ`, 'system');
} else {
addMessageWithAutoScroll(`❌ Ошибка передачи файла: ${error}`, 'system');
}
}
);
}
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange);
@@ -3426,9 +3476,11 @@
setOfferPassword('');
setAnswerPassword('');
if (typeof console.clear === 'function') {
console.clear();
}
// Не очищаем консоль при очистке данных
// чтобы пользователь мог видеть ошибки
// if (typeof console.clear === 'function') {
// console.clear();
// }
// Cleanup pay-per-session state
if (sessionManager) {
@@ -3467,9 +3519,11 @@
setMessages([]);
if (typeof console.clear === 'function') {
console.clear();
}
// Не очищаем консоль при отключении
// чтобы пользователь мог видеть ошибки
// if (typeof console.clear === 'function') {
// console.clear();
// }
document.dispatchEvent(new CustomEvent('peer-disconnect'));
@@ -3565,7 +3619,8 @@
keyFingerprint: keyFingerprint,
isVerified: isVerified,
chatMessagesRef: chatMessagesRef,
scrollToBottom: scrollToBottom
scrollToBottom: scrollToBottom,
webrtcManager: webrtcManagerRef.current
})
: React.createElement(EnhancedConnectionSetup, {
onCreateOffer: handleCreateOffer,
@@ -3629,10 +3684,11 @@
try {
const timestamp = Date.now();
const [cryptoModule, webrtcModule, paymentModule] = await Promise.all([
const [cryptoModule, webrtcModule, paymentModule, fileTransferModule] = await Promise.all([
import(`./src/crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
import(`./src/network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
import(`./src/session/PayPerSessionManager.js?v=${timestamp}`)
import(`./src/session/PayPerSessionManager.js?v=${timestamp}`),
import(`./src/transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`)
]);
const { EnhancedSecureCryptoUtils } = cryptoModule;
@@ -3644,6 +3700,9 @@
const { PayPerSessionManager } = paymentModule;
window.PayPerSessionManager = PayPerSessionManager;
const { EnhancedSecureFileTransfer } = fileTransferModule;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
async function loadReactComponent(path, componentName) {
try {
@@ -3669,7 +3728,8 @@
loadReactComponent('./src/components/ui/SessionTypeSelector.jsx', 'SessionTypeSelector'),
loadReactComponent('./src/components/ui/LightningPayment.jsx', 'LightningPayment'),
loadReactComponent('./src/components/ui/PaymentModal.jsx', 'PaymentModal'),
loadReactComponent('./src/components/ui/DownloadApps.jsx', 'DownloadApps')
loadReactComponent('./src/components/ui/DownloadApps.jsx', 'DownloadApps'),
loadReactComponent('./src/components/ui/FileTransfer.jsx', 'FileTransferComponent')
]);
if (typeof initializeApp === 'function') {
@@ -3711,6 +3771,18 @@ document.addEventListener('session-activated', (event) => {
if (window.forceUpdateHeader) {
window.forceUpdateHeader(event.detail.timeLeft, event.detail.sessionType);
}
// Notify WebRTC Manager about session activation
if (window.webrtcManager && window.webrtcManager.handleSessionActivation) {
console.log('🔐 Notifying WebRTC Manager about session activation');
window.webrtcManager.handleSessionActivation({
sessionId: event.detail.sessionId,
sessionType: event.detail.sessionType,
timeLeft: event.detail.timeLeft,
isDemo: event.detail.isDemo,
sessionManager: window.sessionManager
});
}
});
if (window.DEBUG_MODE) {

View File

@@ -0,0 +1,310 @@
// File Transfer Component for Chat Interface
const FileTransferComponent = ({ webrtcManager, isConnected }) => {
const [dragOver, setDragOver] = React.useState(false);
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
const fileInputRef = React.useRef(null);
// Update transfers periodically
React.useEffect(() => {
if (!isConnected || !webrtcManager) return;
const updateTransfers = () => {
const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers);
};
const interval = setInterval(updateTransfers, 500);
return () => clearInterval(interval);
}, [isConnected, webrtcManager]);
// Setup file transfer callbacks
React.useEffect(() => {
if (!webrtcManager) return;
webrtcManager.setFileTransferCallbacks(
// Progress callback
(progress) => {
const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers);
},
// File received callback
(fileData) => {
// Auto-download received file
const url = URL.createObjectURL(fileData.fileBlob);
const a = document.createElement('a');
a.href = url;
a.download = fileData.fileName;
a.click();
URL.revokeObjectURL(url);
// Update transfer list
const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers);
},
// Error callback
(error) => {
console.error('File transfer error:', error);
const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers);
}
);
}, [webrtcManager]);
const handleFileSelect = async (files) => {
if (!isConnected || !webrtcManager) {
alert('Соединение не установлено. Сначала установите соединение.');
return;
}
// Дополнительная проверка состояния соединения
if (!webrtcManager.isConnected() || !webrtcManager.isVerified) {
alert('Соединение не готово для передачи файлов. Дождитесь завершения установки соединения.');
return;
}
for (const file of files) {
try {
await webrtcManager.sendFile(file);
} catch (error) {
// Более мягкая обработка ошибок - не закрываем сессию
console.error(`Failed to send ${file.name}:`, error);
// Показываем пользователю ошибку, но не закрываем соединение
if (error.message.includes('Connection not ready')) {
alert(`Файл ${file.name} не может быть отправлен сейчас. Проверьте соединение и попробуйте снова.`);
} else if (error.message.includes('File too large')) {
alert(`Файл ${file.name} слишком большой. Максимальный размер: 100 MB`);
} else if (error.message.includes('Maximum concurrent transfers')) {
alert(`Достигнут лимит одновременных передач. Дождитесь завершения текущих передач.`);
} else {
alert(`Ошибка отправки файла ${file.name}: ${error.message}`);
}
}
}
};
const handleDrop = (e) => {
e.preventDefault();
setDragOver(false);
const files = Array.from(e.dataTransfer.files);
handleFileSelect(files);
};
const handleDragOver = (e) => {
e.preventDefault();
setDragOver(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setDragOver(false);
};
const handleFileInputChange = (e) => {
const files = Array.from(e.target.files);
handleFileSelect(files);
e.target.value = ''; // Reset input
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
if (!isConnected) {
return React.createElement('div', {
className: "p-4 text-center text-muted"
}, 'Передача файлов доступна только при установленном соединении');
}
// Проверяем дополнительное состояние соединения
const isConnectionReady = webrtcManager && webrtcManager.isConnected() && webrtcManager.isVerified;
if (!isConnectionReady) {
return React.createElement('div', {
className: "p-4 text-center text-yellow-600"
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-exclamation-triangle mr-2'
}),
'Соединение устанавливается... Передача файлов будет доступна после завершения установки.'
]);
}
return React.createElement('div', {
className: "file-transfer-component"
}, [
// File Drop Zone
React.createElement('div', {
key: 'drop-zone',
className: `file-drop-zone ${dragOver ? 'drag-over' : ''}`,
onDrop: handleDrop,
onDragOver: handleDragOver,
onDragLeave: handleDragLeave,
onClick: () => fileInputRef.current?.click()
}, [
React.createElement('div', {
key: 'drop-content',
className: "drop-content"
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400'
}),
React.createElement('p', {
key: 'text',
className: "text-primary font-medium"
}, 'Перетащите файлы сюда или нажмите для выбора'),
React.createElement('p', {
key: 'subtext',
className: "text-muted text-sm"
}, 'Максимальный размер: 100 МБ на файл')
])
]),
// Hidden file input
React.createElement('input', {
key: 'file-input',
ref: fileInputRef,
type: 'file',
multiple: true,
className: 'hidden',
onChange: handleFileInputChange
}),
// Active Transfers
(transfers.sending.length > 0 || transfers.receiving.length > 0) && React.createElement('div', {
key: 'transfers',
className: "active-transfers mt-4"
}, [
React.createElement('h4', {
key: 'title',
className: "text-primary font-medium mb-3 flex items-center"
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-exchange-alt mr-2'
}),
'Передача файлов'
]),
// Sending files
...transfers.sending.map(transfer =>
React.createElement('div', {
key: `send-${transfer.fileId}`,
className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2"
}, [
React.createElement('div', {
key: 'header',
className: "flex items-center justify-between mb-2"
}, [
React.createElement('div', {
key: 'info',
className: "flex items-center"
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-upload text-blue-400 mr-2'
}),
React.createElement('span', {
key: 'name',
className: "text-primary font-medium text-sm"
}, transfer.fileName),
React.createElement('span', {
key: 'size',
className: "text-muted text-xs ml-2"
}, formatFileSize(transfer.fileSize))
]),
React.createElement('button', {
key: 'cancel',
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
className: "text-red-400 hover:text-red-300 text-xs"
}, [
React.createElement('i', {
className: 'fas fa-times'
})
])
]),
React.createElement('div', {
key: 'progress',
className: "progress-bar"
}, [
React.createElement('div', {
key: 'fill',
className: "progress-fill bg-blue-400",
style: { width: `${transfer.progress}%` }
}),
React.createElement('span', {
key: 'text',
className: "progress-text text-xs"
}, `${transfer.progress.toFixed(1)}% • ${transfer.status}`)
])
])
),
// Receiving files
...transfers.receiving.map(transfer =>
React.createElement('div', {
key: `recv-${transfer.fileId}`,
className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2"
}, [
React.createElement('div', {
key: 'header',
className: "flex items-center justify-between mb-2"
}, [
React.createElement('div', {
key: 'info',
className: "flex items-center"
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-download text-green-400 mr-2'
}),
React.createElement('span', {
key: 'name',
className: "text-primary font-medium text-sm"
}, transfer.fileName),
React.createElement('span', {
key: 'size',
className: "text-muted text-xs ml-2"
}, formatFileSize(transfer.fileSize))
]),
React.createElement('button', {
key: 'cancel',
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
className: "text-red-400 hover:text-red-300 text-xs"
}, [
React.createElement('i', {
className: 'fas fa-times'
})
])
]),
React.createElement('div', {
key: 'progress',
className: "progress-bar"
}, [
React.createElement('div', {
key: 'fill',
className: "progress-fill bg-green-400",
style: { width: `${transfer.progress}%` }
}),
React.createElement('span', {
key: 'text',
className: "progress-text text-xs"
}, `${transfer.progress.toFixed(1)}% • ${transfer.status}`)
])
])
)
])
]);
};
// Export
window.FileTransferComponent = FileTransferComponent;

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,91 @@
flex-direction: column;
}
/* ============================================ */
/* FILE TRANSFER STYLES */
/* ============================================ */
.file-transfer-component {
margin-top: 1rem;
}
.file-drop-zone {
border: 2px dashed #4b5563;
border-radius: 12px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(55, 65, 81, 0.1);
}
.file-drop-zone:hover {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.file-drop-zone.drag-over {
border-color: #10b981;
background: rgba(16, 185, 129, 0.1);
transform: scale(1.02);
}
.drop-content {
pointer-events: none;
}
.active-transfers {
max-height: 300px;
overflow-y: auto;
}
.transfer-item {
transition: all 0.2s ease;
}
.transfer-item:hover {
transform: translateY(-1px);
}
.progress-bar {
position: relative;
height: 6px;
background: rgba(75, 85, 99, 0.3);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 3px;
}
.progress-text {
position: absolute;
top: -20px;
right: 0;
color: #9ca3af;
}
.file-transfer-section {
border-top: 1px solid rgba(75, 85, 99, 0.1);
}
@media (max-width: 640px) {
.file-drop-zone {
padding: 1.5rem;
}
.transfer-item {
padding: 0.75rem;
}
.progress-text {
font-size: 0.75rem;
}
}
.header-minimal {
background: rgb(35 36 35 / 13%);
backdrop-filter: blur(5px);

File diff suppressed because it is too large Load Diff