mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement call encryption
Security fixes
This commit is contained in:
@@ -162,6 +162,24 @@
|
||||
color: $color-dark-on-surface-variant;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.encryption-emojis {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
.encryption-emoji {
|
||||
font-size: 20px;
|
||||
display: inline-block;
|
||||
animation: emoji-pulse 2s ease-in-out infinite;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0s; }
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
&:nth-child(4) { animation-delay: 0.6s; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,4 +393,15 @@
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes emoji-pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useAppState } from "../state";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import * as WebRTC from "@/core/calls/webrtc";
|
||||
import { CallSignalingHandler } from "@/core/calls/signaling";
|
||||
import { setCallSignalingHandler } from "@/core/websocket";
|
||||
import { generateCallSessionKey, generateCallEmojis, createCallSessionKeyFromHash } from "@/core/calls/encryption";
|
||||
import { createRef, useEffect } from "react";
|
||||
|
||||
// Global audio ref shared across all instances
|
||||
let globalRemoteAudioRef = createRef<HTMLAudioElement>();
|
||||
|
||||
export default function useAudioCall() {
|
||||
const { chat, startCall, endCall, setCallStatus, toggleMute, user } = useAppState();
|
||||
const { chat, startCall, endCall, setCallStatus, toggleMute, setCallEncryption, setCallSessionKeyHash, user } = useAppState();
|
||||
const remoteAudioRef = globalRemoteAudioRef;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -23,7 +24,8 @@ export default function useAudioCall() {
|
||||
const state = useAppState.getState();
|
||||
state.receiveCall(userId, username);
|
||||
},
|
||||
endCall
|
||||
endCall,
|
||||
setCallSessionKeyHash
|
||||
}));
|
||||
setCallSignalingHandler(signalingHandler);
|
||||
|
||||
@@ -85,6 +87,14 @@ export default function useAudioCall() {
|
||||
};
|
||||
}, [user.authToken, chat.call.remoteUserId, setCallStatus, endCall, startCall]);
|
||||
|
||||
// Watch for session key hash changes and generate emojis
|
||||
useEffect(() => {
|
||||
if (chat.call.sessionKeyHash && chat.call.encryptionEmojis.length === 0) {
|
||||
const emojis = generateCallEmojis(chat.call.sessionKeyHash);
|
||||
setCallEncryption(chat.call.sessionKeyHash, emojis);
|
||||
}
|
||||
}, [chat.call.sessionKeyHash, chat.call.encryptionEmojis.length, setCallEncryption]);
|
||||
|
||||
async function requestAudioPermissions(): Promise<boolean> {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
@@ -110,14 +120,31 @@ export default function useAudioCall() {
|
||||
}
|
||||
|
||||
|
||||
// Start the call in state
|
||||
startCall(userId, username);
|
||||
setCallStatus("calling");
|
||||
let sessionKey;
|
||||
try {
|
||||
// Generate call session key and emojis
|
||||
sessionKey = await generateCallSessionKey();
|
||||
const emojis = generateCallEmojis(sessionKey.hash);
|
||||
|
||||
// Start the call in state
|
||||
startCall(userId, username);
|
||||
setCallStatus("calling");
|
||||
setCallEncryption(sessionKey.hash, emojis);
|
||||
} catch (error) {
|
||||
console.error("Failed to generate call encryption:", error);
|
||||
endCall();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initiate WebRTC call
|
||||
const success = await WebRTC.initiateCall(userId, username);
|
||||
|
||||
if (!success) {
|
||||
if (success && sessionKey) {
|
||||
// Send session key hash to the receiver for visual verification
|
||||
await WebRTC.sendCallSessionKey(userId, sessionKey.hash);
|
||||
// Also wrap and send the actual session key for E2EE media
|
||||
await WebRTC.sendWrappedCallSessionKey(userId, sessionKey.key, sessionKey.hash);
|
||||
} else {
|
||||
endCall();
|
||||
}
|
||||
};
|
||||
@@ -162,6 +189,7 @@ export default function useAudioCall() {
|
||||
};
|
||||
|
||||
async function handleIncomingCall(userId: number, username: string) {
|
||||
// Don't generate session key here - wait for it from the caller
|
||||
await WebRTC.handleIncomingCall(userId, username);
|
||||
};
|
||||
|
||||
@@ -177,6 +205,17 @@ export default function useAudioCall() {
|
||||
await WebRTC.handleIceCandidate(userId, candidate);
|
||||
};
|
||||
|
||||
async function handleCallSessionKey(sessionKeyHash: string) {
|
||||
try {
|
||||
// Create session key from the hash provided by the caller
|
||||
const sessionKey = await createCallSessionKeyFromHash(sessionKeyHash);
|
||||
const emojis = generateCallEmojis(sessionKey.hash);
|
||||
setCallEncryption(sessionKey.hash, emojis);
|
||||
} catch (error) {
|
||||
console.error("Failed to create call session key from hash:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
call: chat.call,
|
||||
initiateCall,
|
||||
@@ -188,6 +227,7 @@ export default function useAudioCall() {
|
||||
handleCallOffer,
|
||||
handleCallAnswer,
|
||||
handleIceCandidate,
|
||||
handleCallSessionKey,
|
||||
remoteAudioRef
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ interface CallState {
|
||||
remoteUsername: string | null;
|
||||
isInitiator: boolean;
|
||||
isMinimized: boolean;
|
||||
sessionKeyHash: string | null;
|
||||
encryptionEmojis: string[];
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
@@ -75,6 +77,8 @@ interface AppState {
|
||||
toggleMute: () => void;
|
||||
toggleCallMinimize: () => void;
|
||||
receiveCall: (userId: number, username: string) => void;
|
||||
setCallEncryption: (sessionKeyHash: string, encryptionEmojis: string[]) => void;
|
||||
setCallSessionKeyHash: (sessionKeyHash: string) => void;
|
||||
|
||||
// User state
|
||||
user: UserState;
|
||||
@@ -110,7 +114,9 @@ export const useAppState = create<AppState>((set, get) => ({
|
||||
remoteUserId: null,
|
||||
remoteUsername: null,
|
||||
isInitiator: false,
|
||||
isMinimized: false
|
||||
isMinimized: false,
|
||||
sessionKeyHash: null,
|
||||
encryptionEmojis: []
|
||||
}
|
||||
},
|
||||
addMessage: (message: Message) => set((state) => {
|
||||
@@ -404,7 +410,9 @@ export const useAppState = create<AppState>((set, get) => ({
|
||||
remoteUserId: userId,
|
||||
remoteUsername: username,
|
||||
isInitiator: true,
|
||||
isMinimized: false
|
||||
isMinimized: false,
|
||||
sessionKeyHash: null,
|
||||
encryptionEmojis: []
|
||||
}
|
||||
}
|
||||
})),
|
||||
@@ -420,7 +428,9 @@ export const useAppState = create<AppState>((set, get) => ({
|
||||
remoteUserId: null,
|
||||
remoteUsername: null,
|
||||
isInitiator: false,
|
||||
isMinimized: false
|
||||
isMinimized: false,
|
||||
sessionKeyHash: null,
|
||||
encryptionEmojis: []
|
||||
}
|
||||
}
|
||||
})),
|
||||
@@ -467,7 +477,30 @@ export const useAppState = create<AppState>((set, get) => ({
|
||||
remoteUserId: userId,
|
||||
remoteUsername: username,
|
||||
isInitiator: false,
|
||||
isMinimized: false
|
||||
isMinimized: false,
|
||||
sessionKeyHash: null,
|
||||
encryptionEmojis: []
|
||||
}
|
||||
}
|
||||
})),
|
||||
|
||||
setCallEncryption: (sessionKeyHash: string, encryptionEmojis: string[]) => set((state) => ({
|
||||
chat: {
|
||||
...state.chat,
|
||||
call: {
|
||||
...state.chat.call,
|
||||
sessionKeyHash,
|
||||
encryptionEmojis
|
||||
}
|
||||
}
|
||||
})),
|
||||
|
||||
setCallSessionKeyHash: (sessionKeyHash: string) => set((state) => ({
|
||||
chat: {
|
||||
...state.chat,
|
||||
call: {
|
||||
...state.chat.call,
|
||||
sessionKeyHash
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user