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, keyFingerprint,
isVerified, isVerified,
chatMessagesRef, chatMessagesRef,
scrollToBottom scrollToBottom,
webrtcManager
}) => { }) => {
const [showScrollButton, setShowScrollButton] = React.useState(false); const [showScrollButton, setShowScrollButton] = React.useState(false);
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (chatMessagesRef.current && messages.length > 0) { if (chatMessagesRef.current && messages.length > 0) {
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
@@ -2472,7 +2473,28 @@
className: 'fas fa-chevron-down' 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 // Enhanced Chat Input Area
React.createElement('div', { React.createElement('div', {
key: 'chat-input', key: 'chat-input',
@@ -2646,6 +2668,9 @@
}, [sessionManager]); }, [sessionManager]);
const webrtcManagerRef = React.useRef(null); 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 addMessageWithAutoScroll = (message, type) => {
const newMessage = { const newMessage = {
@@ -2880,30 +2905,17 @@
updateSecurityLevel().catch(console.error); updateSecurityLevel().catch(console.error);
} }
} else if (status === 'disconnected') { } else if (status === 'disconnected') {
if (sessionManager && sessionManager.hasActiveSession()) { // При ошибках соединения не сбрасываем сессию полностью
sessionManager.resetSession(); // только обновляем статус соединения
setSessionTimeLeft(0); setConnectionStatus('disconnected');
setHasActiveSession(false);
}
document.dispatchEvent(new CustomEvent('peer-disconnect'));
// Complete UI reset on disconnect
setKeyFingerprint('');
setVerificationCode('');
setSecurityLevel(null);
setIsVerified(false); setIsVerified(false);
setShowVerification(false); setShowVerification(false);
setConnectionStatus('disconnected');
setMessages([]); // Не очищаем консоль и не сбрасываем сообщения
// чтобы пользователь мог видеть ошибки
if (typeof console.clear === 'function') { // Не сбрасываем сессию при ошибках соединения
console.clear(); // только при намеренном отключении
}
setTimeout(() => {
setSessionManager(null);
}, 1000);
} else if (status === 'peer_disconnected') { } else if (status === 'peer_disconnected') {
if (sessionManager && sessionManager.hasActiveSession()) { if (sessionManager && sessionManager.hasActiveSession()) {
sessionManager.resetSession(); sessionManager.resetSession();
@@ -2922,11 +2934,12 @@
setShowVerification(false); setShowVerification(false);
setConnectionStatus('disconnected'); setConnectionStatus('disconnected');
setMessages([]); // Не очищаем сообщения и консоль при отключении пира
// чтобы сохранить историю соединения
if (typeof console.clear === 'function') { // setMessages([]);
console.clear(); // if (typeof console.clear === 'function') {
} // console.clear();
// }
setSessionManager(null); setSessionManager(null);
}, 2000); }, 2000);
@@ -3058,21 +3071,58 @@
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
return () => { // Setup file transfer callbacks
window.removeEventListener('beforeunload', handleBeforeUnload); if (webrtcManagerRef.current) {
document.removeEventListener('visibilitychange', handleVisibilityChange); webrtcManagerRef.current.setFileTransferCallbacks(
// Progress callback
(progress) => {
console.log('File progress:', progress);
},
if (tabSwitchTimeout) { // File received callback
clearTimeout(tabSwitchTimeout); (fileData) => {
tabSwitchTimeout = null; // 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);
if (webrtcManagerRef.current) { addMessageWithAutoScroll(`📥 Файл загружен: ${fileData.fileName}`, 'system');
console.log('🧹 Cleaning up WebRTC Manager...'); },
webrtcManagerRef.current.disconnect();
webrtcManagerRef.current = null; // 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);
if (tabSwitchTimeout) {
clearTimeout(tabSwitchTimeout);
tabSwitchTimeout = null;
}
if (webrtcManagerRef.current) {
console.log('🧹 Cleaning up WebRTC Manager...');
webrtcManagerRef.current.disconnect();
webrtcManagerRef.current = null;
}
};
}, []); // Empty dependency array to run only once }, []); // Empty dependency array to run only once
const ensureActiveSessionOrPurchase = async () => { const ensureActiveSessionOrPurchase = async () => {
@@ -3426,9 +3476,11 @@
setOfferPassword(''); setOfferPassword('');
setAnswerPassword(''); setAnswerPassword('');
if (typeof console.clear === 'function') { // Не очищаем консоль при очистке данных
console.clear(); // чтобы пользователь мог видеть ошибки
} // if (typeof console.clear === 'function') {
// console.clear();
// }
// Cleanup pay-per-session state // Cleanup pay-per-session state
if (sessionManager) { if (sessionManager) {
@@ -3467,9 +3519,11 @@
setMessages([]); setMessages([]);
if (typeof console.clear === 'function') { // Не очищаем консоль при отключении
console.clear(); // чтобы пользователь мог видеть ошибки
} // if (typeof console.clear === 'function') {
// console.clear();
// }
document.dispatchEvent(new CustomEvent('peer-disconnect')); document.dispatchEvent(new CustomEvent('peer-disconnect'));
@@ -3565,7 +3619,8 @@
keyFingerprint: keyFingerprint, keyFingerprint: keyFingerprint,
isVerified: isVerified, isVerified: isVerified,
chatMessagesRef: chatMessagesRef, chatMessagesRef: chatMessagesRef,
scrollToBottom: scrollToBottom scrollToBottom: scrollToBottom,
webrtcManager: webrtcManagerRef.current
}) })
: React.createElement(EnhancedConnectionSetup, { : React.createElement(EnhancedConnectionSetup, {
onCreateOffer: handleCreateOffer, onCreateOffer: handleCreateOffer,
@@ -3629,10 +3684,11 @@
try { try {
const timestamp = Date.now(); 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/crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
import(`./src/network/EnhancedSecureWebRTCManager.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; const { EnhancedSecureCryptoUtils } = cryptoModule;
@@ -3644,6 +3700,9 @@
const { PayPerSessionManager } = paymentModule; const { PayPerSessionManager } = paymentModule;
window.PayPerSessionManager = PayPerSessionManager; window.PayPerSessionManager = PayPerSessionManager;
const { EnhancedSecureFileTransfer } = fileTransferModule;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
async function loadReactComponent(path, componentName) { async function loadReactComponent(path, componentName) {
try { try {
@@ -3669,7 +3728,8 @@
loadReactComponent('./src/components/ui/SessionTypeSelector.jsx', 'SessionTypeSelector'), loadReactComponent('./src/components/ui/SessionTypeSelector.jsx', 'SessionTypeSelector'),
loadReactComponent('./src/components/ui/LightningPayment.jsx', 'LightningPayment'), loadReactComponent('./src/components/ui/LightningPayment.jsx', 'LightningPayment'),
loadReactComponent('./src/components/ui/PaymentModal.jsx', 'PaymentModal'), 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') { if (typeof initializeApp === 'function') {
@@ -3711,6 +3771,18 @@ document.addEventListener('session-activated', (event) => {
if (window.forceUpdateHeader) { if (window.forceUpdateHeader) {
window.forceUpdateHeader(event.detail.timeLeft, event.detail.sessionType); 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) { 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; 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 { .header-minimal {
background: rgb(35 36 35 / 13%); background: rgb(35 36 35 / 13%);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);

File diff suppressed because it is too large Load Diff