<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Group Chat with End-to-End Encryption</title>
</head>
<body>
    <h1>Group Chat with End-to-End Encryption</h1>
    <div>
        <input type="text" id="roomName" placeholder="Enter Room Name">
        <button id="joinBtn">Join Room</button>
    </div>
    <div id="chat" style="display:none;">
        <div id="messages" style="height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 5px;"></div>
        <input type="text" id="messageInput" placeholder="Enter message">
        <button id="sendBtn">Send</button>
    </div>

    <script>
        // Cryptography functions
        let keyPair;
        let sharedGroupKey;
        let peerPublicKeys = {};

        async function generateKeyPair() {
            return window.crypto.subtle.generateKey({
                name: "ECDH",
                namedCurve: "P-256"
            }, true, ["deriveKey", "deriveBits"]);
        }

        async function exportPublicKey(key) {
            const exported = await window.crypto.subtle.exportKey("raw", key);
            return btoa(String.fromCharCode(...new Uint8Array(exported)));
        }

        async function importPublicKey(keyData) {
            const binaryDer = Uint8Array.from(atob(keyData), c => c.charCodeAt(0));
            return window.crypto.subtle.importKey(
                "raw",
                binaryDer,
                {name: "ECDH", namedCurve: "P-256"},
                true,
                []
            );
        }

        async function deriveSharedSecret(privateKey, publicKey) {
            return window.crypto.subtle.deriveBits(
                {
                    name: "ECDH",
                    public: publicKey
                },
                privateKey,
                256    // 256 bits
            );
        }

        async function deriveGroupKey() {
            // Combine all shared secrets to create a group key
            let sharedSecrets = [];
            for (let peerId in peerPublicKeys) {
                let publicKey = peerPublicKeys[peerId];
                let sharedSecret = await deriveSharedSecret(keyPair.privateKey, publicKey);
                sharedSecrets.push(new Uint8Array(sharedSecret));
            }
            // Concatenate shared secrets
            let totalLength = sharedSecrets.reduce((acc, curr) => acc + curr.length, 0);
            let concatenatedSecrets = new Uint8Array(totalLength);
            let offset = 0;
            for (let secret of sharedSecrets) {
                concatenatedSecrets.set(secret, offset);
                offset += secret.length;
            }
            // Hash the concatenated secrets to derive the group key
            const hashBuffer = await window.crypto.subtle.digest('SHA-256', concatenatedSecrets);
            sharedGroupKey = await window.crypto.subtle.importKey(
                'raw',
                hashBuffer,
                {name: 'AES-GCM'},
                false,
                ['encrypt', 'decrypt']
            );
        }

        async function encryptMessage(message) {
            const encoder = new TextEncoder();
            const encodedMessage = encoder.encode(message);
            const iv = window.crypto.getRandomValues(new Uint8Array(12));
            const ciphertext = await window.crypto.subtle.encrypt(
                {name: 'AES-GCM', iv: iv},
                sharedGroupKey,
                encodedMessage
            );
            return {
                iv: btoa(String.fromCharCode(...iv)),
                ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertext)))
            };
        }

        async function decryptMessage(ivBase64, ciphertextBase64) {
            const iv = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0));
            const ciphertext = Uint8Array.from(atob(ciphertextBase64), c => c.charCodeAt(0));
            const decrypted = await window.crypto.subtle.decrypt(
                {name: 'AES-GCM', iv: iv},
                sharedGroupKey,
                ciphertext
            );
            const decoder = new TextDecoder();
            return decoder.decode(decrypted);
        }

        // WebRTC and UI code
        let peerConnections = {};
        let dataChannels = {};
        let localId = null;
        let ws;

        document.getElementById('joinBtn').addEventListener('click', async () => {
            let room = document.getElementById('roomName').value.trim();
            if (room === "") {
                alert("Please enter a room name.");
                return;
            }
            await joinRoom(room);
            document.getElementById('chat').style.display = 'block';
        });

        document.getElementById('sendBtn').addEventListener('click', async () => {
            let message = document.getElementById('messageInput').value;
            if (message.trim() === "") return;
            const encryptedMessage = await encryptMessage(message);
            sendMessageToPeers(JSON.stringify(encryptedMessage));
            addMessageToChat("Me", message);
            document.getElementById('messageInput').value = '';
        });

        async function joinRoom(roomName) {
            keyPair = await generateKeyPair();
            const publicKeyString = await exportPublicKey(keyPair.publicKey);

            ws = new WebSocket(`ws://$p2pchat.fa-n.ru/ws/${roomName}`);
            ws.onopen = () => {
                // Send own public key to server
                ws.send(JSON.stringify({
                    type: 'public-key',
                    key: publicKeyString
                }));
            };
            ws.onmessage = async (event) => {
                let data = JSON.parse(event.data);
                if (data.type === 'init') {
                    localId = data.id;
                    let peers = data.peers;
                    // Import public keys of existing peers
                    for (let peerId in peers) {
                        let publicKey = await importPublicKey(peers[peerId]);
                        peerPublicKeys[peerId] = publicKey;
                    }
                    // Derive group key
                    await deriveGroupKey();
                    // Create peer connections
                    for (let peerId in peerPublicKeys) {
                        if (peerId !== localId) {
                            createPeerConnection(peerId, true);
                        }
                    }
                } else if (data.type === 'new-peer') {
                    let peerId = data.id;
                    let publicKeyString = data.public_key;
                    let publicKey = await importPublicKey(publicKeyString);
                    peerPublicKeys[peerId] = publicKey;
                    // Derive new group key
                    await deriveGroupKey();
                    createPeerConnection(peerId, true);
                } else if (data.type === 'signal') {
                    let from = data.from;
                    let signalData = data.data;
                    await handleSignal(signalData, from);
                } else if (data.type === 'peer-left') {
                    let peerId = data.id;
                    closeConnection(peerId);
                    delete peerPublicKeys[peerId];
                    // Derive new group key
                    await deriveGroupKey();
                }
            };
            ws.onclose = () => {
                console.log('WebSocket connection closed.');
            };
            ws.onerror = (err) => {
                console.error('WebSocket error:', err);
            };
        }

        async function handleSignal(signalData, from) {
            let pc = peerConnections[from];
            if (!pc) {
                pc = createPeerConnection(from, false);
            }
            if (signalData.sdp) {
                await pc.setRemoteDescription(new RTCSessionDescription(signalData.sdp));
                if (pc.remoteDescription.type === 'offer') {
                    let answer = await pc.createAnswer();
                    await pc.setLocalDescription(answer);
                    sendSignal({
                        sdp: pc.localDescription
                    }, from);
                }
            } else if (signalData.candidate) {
                await pc.addIceCandidate(new RTCIceCandidate(signalData.candidate));
            }
        }

        function createPeerConnection(peerId, isInitiator) {
            let pc = new RTCPeerConnection();
            pc.onicecandidate = (event) => {
                if (event.candidate) {
                    sendSignal({
                        candidate: event.candidate
                    }, peerId);
                }
            };
            pc.ondatachannel = (event) => {
                let channel = event.channel;
                setupDataChannel(channel, peerId);
            };
            if (isInitiator) {
                let channel = pc.createDataChannel('chat');
                setupDataChannel(channel, peerId);
                pc.createOffer().then(offer => {
                    pc.setLocalDescription(offer);
                    sendSignal({
                        sdp: offer
                    }, peerId);
                });
            }
            peerConnections[peerId] = pc;
            return pc;
        }

        function setupDataChannel(channel, peerId) {
            channel.onopen = () => {
                console.log('Data channel opened with', peerId);
            };
            channel.onmessage = async (event) => {
                let encryptedMessage = JSON.parse(event.data);
                const message = await decryptMessage(encryptedMessage.iv, encryptedMessage.ciphertext);
                addMessageToChat(`Peer ${peerId.substring(0, 8)}`, message);
            };
            dataChannels[peerId] = channel;
        }

        function sendSignal(data, toId) {
            ws.send(JSON.stringify({
                type: 'signal',
                data: data,
                to: toId
            }));
        }

        function sendMessageToPeers(message) {
            for (let peerId in dataChannels) {
                let channel = dataChannels[peerId];
                if (channel.readyState === 'open') {
                    channel.send(message);
                }
            }
        }

        function addMessageToChat(sender, message) {
            let messagesDiv = document.getElementById('messages');
            let messageElem = document.createElement('div');
            messageElem.textContent = `${sender}: ${message}`;
            messagesDiv.appendChild(messageElem);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        function closeConnection(peerId) {
            console.log(`Peer ${peerId} has left.`);
            if (dataChannels[peerId]) {
                dataChannels[peerId].close();
                delete dataChannels[peerId];
            }
            if (peerConnections[peerId]) {
                peerConnections[peerId].close();
                delete peerConnections[peerId];
            }
        }

    </script>
</body>
</html># server.py

import asyncio
import json
import uuid
from aiohttp import web

ROOMS = {}  # room_name -> dict of client_id -> websocket and public key

async def websocket_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)

    room_name = request.match_info.get('room')
    if room_name not in ROOMS:
        ROOMS[room_name] = {}
    room = ROOMS[room_name]

    # Assign a unique ID to the client
    client_id = str(uuid.uuid4())
    room[client_id] = {'ws': ws}

    try:
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                data = json.loads(msg.data)
                message_type = data.get('type')
                if message_type == 'public-key':
                    # Store the client's public key
                    room[client_id]['public_key'] = data['key']
                    # Send initialization data to the new client
                    public_keys = {cid: cdata['public_key'] for cid, cdata in room.items() if 'public_key' in cdata}
                    await ws.send_json({
                        'type': 'init',
                        'id': client_id,
                        'peers': public_keys
                    })
                    # Notify other clients about the new client
                    for cid, cdata in room.items():
                        if cid != client_id and 'public_key' in cdata:
                            await cdata['ws'].send_json({
                                'type': 'new-peer',
                                'id': client_id,
                                'public_key': data['key']
                            })
                elif message_type == 'signal':
                    to_id = data['to']
                    if to_id in room:
                        await room[to_id]['ws'].send_json({
                            'type': 'signal',
                            'from': client_id,
                            'data': data['data']
                        })
            elif msg.type == web.WSMsgType.ERROR:
                print(f'WebSocket connection closed with exception {ws.exception()}')
    finally:
        del room[client_id]
        if not room:
            del ROOMS[room_name]
        else:
            # Notify remaining clients that this client has left
            for cdata in room.values():
                if 'ws' in cdata:
                    await cdata['ws'].send_json({
                        'type': 'peer-left',
                        'id': client_id
                    })

    return ws

app = web.Application()
app.router.add_get('/ws/{room}', websocket_handler)

if __name__ == '__main__':
    web.run_app(app, port=6789) ошибка в firefox: Uncaught (in promise) DOMException: Cannot set remote answer in state stable
    handleSignal 
    onmessage 
    joinRoom
    async* 
    EventListener.handleEvent*
chat:203
Uncaught (in promise) DOMException: Unknown ufrag (oVFa) 
WebRTC: ICE failed, add a STUN server and see about:webrtc for more details ошибка в chrome: Uncaught (in promise) InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable исправь ошибку