Обмен ключами шифрования DH

This commit is contained in:
RoyceDa
2026-02-28 17:33:23 +02:00
parent 461ccbfa94
commit 9eac2fae6f
6 changed files with 271 additions and 8 deletions

View File

@@ -1,5 +1,11 @@
import { Call } from "@/app/components/Call/Call";
import { createContext, useState } from "react";
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 {
@@ -13,15 +19,29 @@ export interface CallContextValue {
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<CallContextValue | null>(null);
export interface CallProviderProps {
children: React.ReactNode;
@@ -34,18 +54,142 @@ export function CallProvider(props : CallProviderProps) {
const [sound, setSound] = useState<boolean>(true);
const [duration, setDuration] = useState<number>(0);
const [showCallView, setShowCallView] = useState<boolean>(callState == CallState.INCOMING);
const {info} = useConsoleLogger("CallProvider");
const [sessionKeys, setSessionKeys] = useState<nacl.BoxKeyPair | null>(null);
const send = useSender();
const publicKey = usePublicKey();
const roleRef = useRef<CallRole | null>(null);
const [sharedSecret, setSharedSecret] = useState<string>("");
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.ACTIVE_CALL) {
setCallState(CallState.ACTIVE);
}
}, []);
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(!sessionKeys){
return "";
}
return Buffer.from(sessionKeys.secretKey).toString('hex');
}
const context = {
@@ -58,7 +202,9 @@ export function CallProvider(props : CallProviderProps) {
setMuted,
setSound,
duration,
setShowCallView
setShowCallView,
getKeyCast,
accept
};
return (