import React, { useState, useEffect, useRef } from 'react'; import { Shield, Wifi, Send, Paperclip, Lock, X, Download, Server, Zap, Activity, Trash2, FileText } from 'lucide-react'; // Firebase Imports import { initializeApp } from 'firebase/app'; import { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, updateDoc, getDoc } from 'firebase/firestore'; import { getAuth, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; // --- DEINE KONFIGURATION (HIER EINFÜGEN) --- const FIREBASE_CONFIG = { // Ersetze dies mit deinen Daten aus der Firebase Console: apiKey: "DEIN_API_KEY_HIER", authDomain: "dein-projekt.firebaseapp.com", projectId: "dein-projekt", storageBucket: "dein-projekt.appspot.com", messagingSenderId: "123456789", appId: "1:12345:web:abcdef" }; const APP_ID = "secure-p2p-chat"; // Ein eindeutiger Name für deine App // --- WEB RTC KONFIGURATION --- const RTC_CONFIG = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:global.stun.twilio.com:3478' } ] }; const CHUNK_SIZE = 16 * 1024; // 16KB Chunks (Safe for WebRTC) // --- CRYPTO HELPERS (Native Web Crypto) --- const generateKeyPair = async () => { return await window.crypto.subtle.generateKey( { name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey", "deriveBits"] ); }; const exportKey = async (key) => { const exported = await window.crypto.subtle.exportKey("spki", key); return btoa(String.fromCharCode(...new Uint8Array(exported))); }; const importKey = async (base64Key) => { const binary = atob(base64Key); const buffer = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) buffer[i] = binary.charCodeAt(i); return await window.crypto.subtle.importKey( "spki", buffer, { name: "ECDH", namedCurve: "P-256" }, true, [] ); }; const deriveSessionKey = async (privateKey, publicKey) => { return await window.crypto.subtle.deriveKey( { name: "ECDH", public: publicKey }, privateKey, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); }; // --- COMPONENT --- export default function HighPerformanceP2P() { // Global Setup const [user, setUser] = useState(null); const [db, setDb] = useState(null); // State const [myCode, setMyCode] = useState(''); const [targetCode, setTargetCode] = useState(''); const [status, setStatus] = useState('idle'); // idle, signaling, connected const [messages, setMessages] = useState([]); const [textInput, setTextInput] = useState(''); const [sessionKey, setSessionKey] = useState(null); const [fingerprint, setFingerprint] = useState(''); // File Transfer State const [transfers, setTransfers] = useState({}); // id -> { progress, type, name } // Refs const rtcPeer = useRef(null); const dataChannel = useRef(null); const keyPair = useRef(null); const signalDocUnsub = useRef(null); // Receive Buffer const incomingFiles = useRef({}); // --- FIREBASE INIT --- useEffect(() => { try { // Nutze die Konstante von oben statt der Umgebungsvariable if (FIREBASE_CONFIG.apiKey === "DEIN_API_KEY_HIER") { console.warn("Bitte trage deine Firebase-Config im Code ein!"); } const app = initializeApp(FIREBASE_CONFIG); const auth = getAuth(app); setDb(getFirestore(app)); // Standard Anonyme Anmeldung für einfaches Hosting signInAnonymously(auth).catch((err) => { console.error("Fehler bei der Anmeldung:", err); }); const unsub = onAuthStateChanged(auth, (u) => { if (u) { setUser(u); const randomCode = Math.random().toString(36).substring(2, 10); setMyCode(randomCode); } }); return () => unsub(); } catch (e) { console.error("Firebase Init Error", e); } }, []); // --- WEBRTC SETUP --- const initWebRTC = async (isInitiator) => { rtcPeer.current = new RTCPeerConnection(RTC_CONFIG); rtcPeer.current.onicecandidate = (event) => { if (event.candidate) { sendSignalData({ type: 'candidate', candidate: JSON.stringify(event.candidate) }); } }; rtcPeer.current.onconnectionstatechange = () => { if (rtcPeer.current.connectionState === 'disconnected') { setStatus('idle'); setMessages(prev => [...prev, { system: true, text: 'Verbindung verloren.' }]); cleanup(); } }; if (isInitiator) { const channel = rtcPeer.current.createDataChannel("secure-chat"); setupDataChannel(channel); const offer = await rtcPeer.current.createOffer(); await rtcPeer.current.setLocalDescription(offer); sendSignalData({ type: 'offer', sdp: JSON.stringify(offer) }); } else { rtcPeer.current.ondatachannel = (event) => { setupDataChannel(event.channel); }; } }; const setupDataChannel = (channel) => { dataChannel.current = channel; channel.binaryType = 'arraybuffer'; channel.onopen = async () => { setStatus('connected'); // Bereinige Signalling-Daten if (myCode && db && user) { if (signalDocUnsub.current) signalDocUnsub.current(); } keyPair.current = await generateKeyPair(); const pubKey = await exportKey(keyPair.current.publicKey); channel.send(JSON.stringify({ type: 'key-exchange', key: pubKey })); }; channel.onmessage = handleDataMessage; }; // --- SIGNALING (FIREBASE) --- const sendSignalData = async (data) => { if (!db || !user) return; const roomId = targetCode || myCode; const isHost = !targetCode; // Pfad angepasst für eigene Struktur const signalRef = doc(db, 'chat_signals', `signal_${roomId}`); try { if (data.type === 'offer') { await setDoc(signalRef, { guestSDP: data.sdp, guestCandidates: [], created: Date.now() }, { merge: true }); } else if (data.type === 'answer') { await setDoc(signalRef, { hostSDP: data.sdp, hostCandidates: [] }, { merge: true }); } else if (data.type === 'candidate') { const snap = await getDoc(signalRef); if (snap.exists()) { const current = snap.data(); if (isHost) { const list = current.hostCandidates || []; await updateDoc(signalRef, { hostCandidates: [...list, data.candidate] }); } else { const list = current.guestCandidates || []; await updateDoc(signalRef, { guestCandidates: [...list, data.candidate] }); } } } } catch (err) { console.error("Signaling Error (Check Permissions):", err); } }; const startHosting = async () => { if (!db || !user) return; setStatus('signaling'); const roomId = myCode; // Pfad angepasst: 'chat_signals' Collection im Root const signalRef = doc(db, 'chat_signals', `signal_${roomId}`); await setDoc(signalRef, { created: Date.now() }); const unsub = onSnapshot(signalRef, async (snap) => { const data = snap.data(); if (!data) return; if (data.guestSDP && !rtcPeer.current) { await initWebRTC(false); const offer = JSON.parse(data.guestSDP); await rtcPeer.current.setRemoteDescription(offer); const answer = await rtcPeer.current.createAnswer(); await rtcPeer.current.setLocalDescription(answer); sendSignalData({ type: 'answer', sdp: JSON.stringify(answer) }); } if (data.guestCandidates && rtcPeer.current) { data.guestCandidates.forEach(async c => { try { await rtcPeer.current.addIceCandidate(JSON.parse(c)); } catch(e) {} }); } }); signalDocUnsub.current = unsub; }; const joinSession = async () => { if (!targetCode || !db) return; setStatus('signaling'); const roomId = targetCode; const signalRef = doc(db, 'chat_signals', `signal_${roomId}`); await initWebRTC(true); const unsub = onSnapshot(signalRef, async (snap) => { const data = snap.data(); if (!data) return; if (data.hostSDP && rtcPeer.current && rtcPeer.current.signalingState === 'have-local-offer') { const answer = JSON.parse(data.hostSDP); await rtcPeer.current.setRemoteDescription(answer); } if (data.hostCandidates && rtcPeer.current) { data.hostCandidates.forEach(async c => { try { await rtcPeer.current.addIceCandidate(JSON.parse(c)); } catch(e) {} }); } }); signalDocUnsub.current = unsub; }; // --- DATA CHANNEL & CRYPTO --- const handleDataMessage = async (event) => { const buffer = event.data; if (typeof buffer === 'string') { const msg = JSON.parse(buffer); if (msg.type === 'key-exchange') { const theirKey = await importKey(msg.key); const shared = await deriveSessionKey(keyPair.current.privateKey, theirKey); setSessionKey(shared); const raw = await window.crypto.subtle.exportKey("raw", shared); const hash = await window.crypto.subtle.digest("SHA-256", raw); setFingerprint(Array.from(new Uint8Array(hash)).map(b=>b.toString(16).padStart(2,'0')).join('').substring(0,8).toUpperCase()); } else if (msg.type === 'chat') { const decrypted = await decryptData(msg.iv, msg.content); setMessages(p => [...p, { sender: 'remote', content: decrypted, type: 'text' }]); } else if (msg.type === 'file-start') { incomingFiles.current[msg.id] = { chunks: [], meta: msg, receivedSize: 0, startTime: Date.now() }; setTransfers(prev => ({ ...prev, [msg.id]: { progress: 0, type: 'receive', name: msg.name, size: msg.size } })); } else if (msg.type === 'file-end') { const fileData = incomingFiles.current[msg.id]; if (fileData) { const blob = new Blob(fileData.chunks); const url = URL.createObjectURL(blob); setMessages(p => [...p, { sender: 'remote', content: url, type: 'file', fileName: fileData.meta.name, fileSize: fileData.meta.size }]); delete incomingFiles.current[msg.id]; setTransfers(prev => { const next = { ...prev }; delete next[msg.id]; return next; }); } } return; } const activeFileId = Object.keys(incomingFiles.current)[0]; if (!activeFileId || !sessionKey) return; const iv = buffer.slice(0, 12); const content = buffer.slice(12); try { const decryptedChunk = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(iv) }, sessionKey, content ); const fileRecord = incomingFiles.current[activeFileId]; fileRecord.chunks.push(decryptedChunk); fileRecord.receivedSize += decryptedChunk.byteLength; setTransfers(prev => ({ ...prev, [activeFileId]: { ...prev[activeFileId], progress: (fileRecord.receivedSize / fileRecord.meta.size) * 100 } })); } catch (e) { console.error("Decrypt chunk failed", e); } }; const encryptData = async (text) => { const enc = new TextEncoder().encode(text); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const content = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv }, sessionKey, enc ); return { iv: btoa(String.fromCharCode(...iv)), content: btoa(String.fromCharCode(...new Uint8Array(content))) }; }; const decryptData = async (ivB64, contentB64) => { const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const content = Uint8Array.from(atob(contentB64), c => c.charCodeAt(0)); const dec = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv }, sessionKey, content ); return new TextDecoder().decode(dec); }; const sendMessage = async () => { if (!textInput.trim() || !sessionKey) return; const encrypted = await encryptData(textInput); dataChannel.current.send(JSON.stringify({ type: 'chat', iv: encrypted.iv, content: encrypted.content })); setMessages(p => [...p, { sender: 'me', content: textInput, type: 'text' }]); setTextInput(''); }; const sendFile = async (file) => { if (!sessionKey || !dataChannel.current) return; const fileId = Math.random().toString(36).substring(7); dataChannel.current.send(JSON.stringify({ type: 'file-start', id: fileId, name: file.name, size: file.size })); setTransfers(prev => ({ ...prev, [fileId]: { progress: 0, type: 'send', name: file.name, size: file.size } })); const reader = new FileReader(); let offset = 0; const readSlice = (o) => { const slice = file.slice(o, o + CHUNK_SIZE); reader.readAsArrayBuffer(slice); }; reader.onload = async (e) => { const buffer = e.target.result; const iv = window.crypto.getRandomValues(new Uint8Array(12)); const encryptedBuf = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv }, sessionKey, buffer ); const packet = new Uint8Array(12 + encryptedBuf.byteLength); packet.set(iv, 0); packet.set(new Uint8Array(encryptedBuf), 12); if (dataChannel.current.bufferedAmount > 16 * 1024 * 1024) { await new Promise(r => setTimeout(r, 100)); } try { dataChannel.current.send(packet); } catch(err) { console.error("Send error", err); return; } offset += CHUNK_SIZE; setTransfers(prev => ({ ...prev, [fileId]: { ...prev[fileId], progress: Math.min(100, (offset / file.size) * 100) } })); if (offset < file.size) { readSlice(offset); } else { dataChannel.current.send(JSON.stringify({ type: 'file-end', id: fileId })); setTransfers(prev => { const next = { ...prev }; delete next[fileId]; return next; }); } }; readSlice(0); }; const cleanup = () => { if(signalDocUnsub.current) signalDocUnsub.current(); if(rtcPeer.current) rtcPeer.current.close(); setSessionKey(null); setFingerprint(''); }; const renderLogin = () => (

SECURE.STREAM.P2P

{myCode || '...'}
ODER
setTargetCode(e.target.value)} placeholder="Partner ID eingeben..." className="w-full bg-neutral-900 border border-neutral-700 p-3 rounded text-white focus:border-emerald-500 outline-none" />
{status === 'signaling' && (
Signaling via Firestore... Tausche Schlüssel aus...
)}
); if (status !== 'connected') return renderLogin(); return (
{/* Header */}

SECURE LINK ESTABLISHED

AES-GCM-256 | DIRECT P2P

FINGERPRINT {fingerprint || 'AUTH...'}
{/* Chat Area */}
Kein Server speichert deine Daten. Alles fließt direkt.
{messages.map((m, i) => (
{m.type === 'text' && (
{m.content}
)} {m.type === 'file' && (
{m.fileName}
{(m.fileSize / (1024*1024)).toFixed(2)} MB
)}
))} {/* Active Transfers */} {Object.entries(transfers).map(([id, t]) => (
{t.type === 'send' ? 'Sende' : 'Empfange'}: {t.name} {t.progress.toFixed(1)}%
))}
{/* Input */}
setTextInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && sendMessage()} />
); }