import { Call } from "@/app/components/Call/Call"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { createContext, useEffect, useRef, useState } from "react"; import nacl from 'tweetnacl'; import { useSender } from "../ProtocolProvider/useSender"; import { PacketSignal, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal"; import { usePacket } from "../ProtocolProvider/usePacket"; import { usePublicKey } from "../AccountProvider/usePublicKey"; export interface CallContextValue { call: (callable: string) => void; close: () => void; activeCall: string; callState: CallState; muted: boolean; sound: boolean; setMuted: (muted: boolean) => void; setSound: (sound: boolean) => void; duration: number; setShowCallView: (show: boolean) => void; getKeyCast: () => string; accept: () => void; } export enum CallState { CONNECTING, KEY_EXCHANGE, ACTIVE, ENDED, INCOMING } export enum CallRole { /** * Вызывающая сторона, которая инициирует звонок */ CALLER, /** * Принимающая сторона, которая отвечает на звонок и принимает его */ CALLEE } export const CallContext = createContext(null); export interface CallProviderProps { children: React.ReactNode; } export function CallProvider(props : CallProviderProps) { const [activeCall, setActiveCall] = useState(""); const [callState, setCallState] = useState(CallState.ENDED); const [muted, setMuted] = useState(false); const [sound, setSound] = useState(true); const [duration, setDuration] = useState(0); const [showCallView, setShowCallView] = useState(callState == CallState.INCOMING); const {info} = useConsoleLogger("CallProvider"); const [sessionKeys, setSessionKeys] = useState(null); const send = useSender(); const publicKey = usePublicKey(); const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); useEffect(() => { console.info("TRACE -> ", sharedSecret) }, [sharedSecret]); usePacket(26, (packet: PacketSignal) => { const signalType = packet.getSignalType(); if(activeCall){ /** * У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка */ if(packet.getSrc() != activeCall){ info("Received signal for another call, ignoring"); return; } } if(signalType == SignalType.CALL){ /** * Нам поступает звонок */ setActiveCall(packet.getSrc()); setCallState(CallState.INCOMING); setShowCallView(true); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){ /** * Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть, * теперь мы можем создать общую секретную сессию для шифрования звонка */ const sharedPublic = packet.getSharedPublic(); if(!sharedPublic){ info("Received key exchange signal without shared public key"); return; } const sessionKeys = generateSessionKeys(); const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); /** * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию */ const signalPacket = new PacketSignal(); signalPacket.setSrc(publicKey); signalPacket.setDst(packet.getDst()); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex')); send(signalPacket); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ /** * Мы отправили свою публичную часть ключа другой стороне, * теперь мы получили ее публичную часть и можем создать общую * секретную сессию для шифрования звонка */ const sharedPublic = packet.getSharedPublic(); if(!sharedPublic){ info("Received key exchange signal without shared public key"); return; } if(!sessionKeys){ info("Received key exchange signal but session keys are not generated"); return; } const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex')); setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); } if(signalType == SignalType.OFFER){ const offerJson = packet.getOffer(); if(!offerJson || !peerConnectionRef.current){ info("Received offer but peer connection is not ready"); return; } handleOffer(offerJson); } if(signalType == SignalType.ANSWER){ const answerJson = packet.getAnswer(); if(!answerJson || !peerConnectionRef.current){ info("Received answer but peer connection is not ready"); return; } handleAnswer(answerJson); } if(signalType == SignalType.ACTIVE_CALL) { setCallState(CallState.ACTIVE); } }, [activeCall, sessionKeys]); const generateSessionKeys = () => { const sessionKeys = nacl.box.keyPair(); info("Generated keys for call session, len: " + sessionKeys.publicKey.length); setSessionKeys(sessionKeys); return sessionKeys; } const call = (dialog: string) => { setActiveCall(dialog); setCallState(CallState.CONNECTING); setShowCallView(true); const signalPacket = new PacketSignal(); signalPacket.setDst(dialog); signalPacket.setSignalType(SignalType.CALL); send(signalPacket); roleRef.current = CallRole.CALLER; } const close = () => { setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); setSessionKeys(null); setDuration(0); roleRef.current = null; } const accept = () => { if(callState != CallState.INCOMING){ /** * Нечего принимать */ return; } /** * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи */ const keys = generateSessionKeys(); const signalPacket = new PacketSignal(); signalPacket.setDst(activeCall); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex')); send(signalPacket); setCallState(CallState.KEY_EXCHANGE); roleRef.current = CallRole.CALLEE; } /** * Получает слепок ключа для отображения в UI * чтобы не показывать настоящий ключ * @returns */ const getKeyCast = () => { if(!sharedSecret){ return ""; } return sharedSecret; } const context = { call, close, activeCall, callState, muted, sound, setMuted, setSound, duration, setShowCallView, getKeyCast, accept }; return ( {props.children} {showCallView && } ) }