код работает только если 2 пользователя в  чате, если больше то chat/:149 Decryption failed: OperationError <!-- 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 = {};
		let isGroupKeyReady = false;
		let groupKeyVersion = 0;
        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() {
    isGroupKeyReady = false; // Disable message sending during key derivation

    // Sort peer IDs to ensure consistent ordering
    let peerIds = Object.keys(peerPublicKeys).concat(localId).sort(); // Include own ID
    // Ensure no duplicates
    peerIds = [...new Set(peerIds)];

    // Compute groupKeyVersion as a hash of the sorted peer IDs
    const encoder = new TextEncoder();
    const peerIdsString = peerIds.join(',');
    const peerIdsBuffer = encoder.encode(peerIdsString);
    const versionHashBuffer = await window.crypto.subtle.digest('SHA-256', peerIdsBuffer);
    const versionArray = new Uint8Array(versionHashBuffer);
    // Convert the hash to a hex string
    groupKeyVersion = Array.from(versionArray).map(b => b.toString(16).padStart(2, '0')).join('');

    console.log(`Group Key Version: ${groupKeyVersion}`);

    // Now derive shared secrets with all peers and combine them
    let sharedSecrets = [];
    for (let peerId of peerIds) {
        if (peerId === localId) continue; // Skip self
        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'},
        true, // <- Make the key extractable
        ['encrypt', 'decrypt']
    );

    // Optionally, log the group key hash for debugging
    const keyBuffer = await window.crypto.subtle.exportKey('raw', sharedGroupKey);
    const keyHashBuffer = await window.crypto.subtle.digest('SHA-256', keyBuffer);
    const keyHashArray = new Uint8Array(keyHashBuffer);
    const keyHashHex = Array.from(keyHashArray).map(b => b.toString(16).padStart(2, '0')).join('');
    console.log(`Derived group key hash: ${keyHashHex}`);

    isGroupKeyReady = true; // Re-enable message sending
}



        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))),
				version: groupKeyVersion // Include deterministic version
			};
		}


        async function decryptMessage(ivBase64, ciphertextBase64) {
			try {
				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);
			} catch (error) {
				console.error("Decryption failed:", error);
				throw error; // Re-throw after logging
			}
		}

        // 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 () => {
			if (!isGroupKeyReady) {
				alert("Group key is not ready yet. Please wait.");
				return;
			}
			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(`wss://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); // No longer pass isInitiator
					}
				}
				}
				 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();
					// Then create peer connection
					createPeerConnection(peerId);
				}
				 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);
			}
			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) {
    const isInitiator = localId > peerId; // Initiator if localId is greater
    let pc = new RTCPeerConnection({
        iceServers: [{
            urls: 'stun:stun.l.google.com:19302'
        }]
    });

    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);
				let processMessage = async () => {
					if (encryptedMessage.version !== groupKeyVersion) {
						console.warn("Received message with outdated group key version.");
						return; // Ignore or handle as needed
					}
					try {
						const message = await decryptMessage(encryptedMessage.iv, encryptedMessage.ciphertext);
						addMessageToChat(`Peer ${peerId.substring(0, 8)}`, message);
					} catch (error) {
						console.error("Decryption failed:", error);
					}
				};
				if (!isGroupKeyReady) {
					// Buffer the message until the group key is ready
					let interval = setInterval(() => {
						if (isGroupKeyReady) {
							clearInterval(interval);
							processMessage();
						}
					}, 100);
				} else {
					processMessage();
				}
			};
			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']
                        })
                    else:
                        print(f"Peer {to_id} not found in room {room_name}.")

            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)