Implement call encryption

Security fixes
This commit is contained in:
2025-10-01 22:50:32 +03:00
Unverified
parent a424ea009d
commit 1b2f825a74
9 changed files with 657 additions and 27 deletions
@@ -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;
}
}
+47 -7
View File
@@ -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
};
}
+37 -4
View File
@@ -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
}
}
}))