release: v4.8.7 WebRTC join reliability patch
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

This commit is contained in:
lockbitchat
2026-05-19 09:49:22 -04:00
parent 1cc873223a
commit 2468cb495e
17 changed files with 2093 additions and 217 deletions
+21 -2
View File
@@ -1,6 +1,25 @@
# Changelog # Changelog
## v4.8.6Security hardening patch release ## v4.8.7WebRTC manual join reliability patch
This patch improves manual WebRTC setup across separate devices and restrictive local networks.
### Fixed
- Stabilized the manual offer/answer join flow so verification waits for real transport readiness.
- Preserved generated response data during manual exchange instead of resetting the joiner screen prematurely.
- Preserved pending creator-side offer context so responses can be applied after transient ICE failures without false session-salt hijacking errors.
- Added operator ICE override support through `config/ice-servers.js`.
- Added ExpressTURN TURN/STUN configuration for relay fallback in environments where mDNS host candidates cannot connect.
- Added user-visible warning when a remote peer provides only mDNS host candidates and no `srflx` or `relay` route.
- Added safer ICE diagnostics that report candidate classes without exposing full IP addresses or TURN credentials.
### Verification
- `npm test`
- `npm run build`
## v4.8.7 — Security hardening patch release
This patch release strengthens SecureBit.chat across verification, sanitization, privacy, transport abuse resistance, cache safety, and repository hygiene. This patch release strengthens SecureBit.chat across verification, sanitization, privacy, transport abuse resistance, cache safety, and repository hygiene.
@@ -29,7 +48,7 @@ This patch release strengthens SecureBit.chat across verification, sanitization,
- Clean install succeeds with `npm ci`. - Clean install succeeds with `npm ci`.
- Production build succeeds with `npm run build`. - Production build succeeds with `npm run build`.
## v4.8.5 — Security hardening release ## v4.8.7 — Security hardening release
This release consolidates several months of security, privacy, and lifecycle hardening work by the SecureBit.chat team. This release consolidates several months of security, privacy, and lifecycle hardening work by the SecureBit.chat team.
+6 -2
View File
@@ -1,4 +1,4 @@
# SecureBit.chat v4.8.6 # SecureBit.chat v4.8.7
SecureBit.chat is a browser-based peer-to-peer chat application built on WebRTC and Web Crypto APIs. It is designed for direct encrypted communication, explicit peer verification, and a small operational footprint without account registration or server-side message storage. SecureBit.chat is a browser-based peer-to-peer chat application built on WebRTC and Web Crypto APIs. It is designed for direct encrypted communication, explicit peer verification, and a small operational footprint without account registration or server-side message storage.
@@ -15,7 +15,11 @@ SecureBit.chat uses:
A session is not treated as verified until both peers complete the interactive SAS flow. Each user must compare the displayed code with the peer through an out-of-band channel and enter the matching code manually. Three failed SAS attempts terminate the session. A session is not treated as verified until both peers complete the interactive SAS flow. Each user must compare the displayed code with the peer through an out-of-band channel and enter the matching code manually. Three failed SAS attempts terminate the session.
## Highlights in v4.8.6 ## Highlights in v4.8.7
- Manual WebRTC setup now preserves pending offer/answer state during slow out-of-band exchange.
- TURN relay fallback can be configured through `config/ice-servers.js` for restrictive networks.
- ICE diagnostics now identify mDNS-only candidate failures without exposing full peer IPs.
This patch release strengthens the existing security model with a focused hardening pass: This patch release strengthens the existing security model with a focused hardening pass:
+15
View File
@@ -0,0 +1,15 @@
// SecureBit.chat operator ICE server override.
// Loaded before the WebRTC manager is created. Credentials are visible to browsers;
// rotate them from the ExpressTURN dashboard if this file is published publicly.
window.SECUREBIT_ICE_SERVERS = [
{ urls: 'stun:stun.cloudflare.com:3478' },
{ urls: 'stun:stun.expressturn.com:3478' },
{
urls: [
'turn:free.expressturn.com:3478?transport=udp',
'turn:free.expressturn.com:3478?transport=tcp'
],
username: '000000002094555952',
credential: 't1oK9Zftes9j7E7hJmsLad9jq1M='
}
];
+1646 -110
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
File diff suppressed because one or more lines are too long
Vendored
+91 -68
View File
@@ -1,3 +1,36 @@
// src/utils/debugWindowHooks.js
function isSecureBitDebugEnabled(targetWindow = globalThis.window) {
return targetWindow?.SECUREBIT_DEBUG === true;
}
function installDebugWindowHooks({
targetWindow = globalThis.window,
webrtcManagerRef,
onClearData,
clearConsole = () => {
if (typeof console.clear === "function") {
console.clear();
}
}
}) {
if (!isSecureBitDebugEnabled(targetWindow)) {
return () => {
};
}
targetWindow.forceCleanup = () => {
onClearData();
if (webrtcManagerRef.current) {
webrtcManagerRef.current.disconnect();
}
};
targetWindow.clearLogs = clearConsole;
targetWindow.webrtcManagerRef = webrtcManagerRef;
return () => {
delete targetWindow.forceCleanup;
delete targetWindow.clearLogs;
delete targetWindow.webrtcManagerRef;
};
}
// src/app.jsx // src/app.jsx
var EnhancedCopyButton = ({ text, className = "", children }) => { var EnhancedCopyButton = ({ text, className = "", children }) => {
const [copied, setCopied] = React.useState(false); const [copied, setCopied] = React.useState(false);
@@ -322,9 +355,11 @@ var EnhancedConnectionSetup = ({
markAnswerCreated, markAnswerCreated,
notificationIntegrationRef, notificationIntegrationRef,
isGeneratingKeys, isGeneratingKeys,
setIsGeneratingKeys,
handleCreateOffer, handleCreateOffer,
relayOnlyMode, relayOnlyMode,
setRelayOnlyMode setRelayOnlyMode,
webrtcManagerRef
}) => { }) => {
const [mode, setMode] = React.useState("select"); const [mode, setMode] = React.useState("select");
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false); const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
@@ -714,6 +749,7 @@ var EnhancedConnectionSetup = ({
text: (() => { text: (() => {
try { try {
const min = typeof offerData === "object" ? JSON.stringify(offerData) : offerData || ""; const min = typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
if (!min) return "";
if (typeof window.encodeBinaryToPrefixed === "function") { if (typeof window.encodeBinaryToPrefixed === "function") {
return window.encodeBinaryToPrefixed(min); return window.encodeBinaryToPrefixed(min);
} }
@@ -1032,6 +1068,7 @@ var EnhancedConnectionSetup = ({
text: (() => { text: (() => {
try { try {
const min = typeof answerData === "object" ? JSON.stringify(answerData) : answerData || ""; const min = typeof answerData === "object" ? JSON.stringify(answerData) : answerData || "";
if (!min) return "";
if (typeof window.encodeBinaryToPrefixed === "function") { if (typeof window.encodeBinaryToPrefixed === "function") {
return window.encodeBinaryToPrefixed(min); return window.encodeBinaryToPrefixed(min);
} }
@@ -1449,7 +1486,7 @@ var EnhancedSecureP2PChat = () => {
const [qrCodeUrl, setQrCodeUrl] = React.useState(""); const [qrCodeUrl, setQrCodeUrl] = React.useState("");
const [showQRScanner, setShowQRScanner] = React.useState(false); const [showQRScanner, setShowQRScanner] = React.useState(false);
const [showQRScannerModal, setShowQRScannerModal] = React.useState(false); const [showQRScannerModal, setShowQRScannerModal] = React.useState(false);
const [isGeneratingKeys, setIsGeneratingKeys2] = React.useState(false); const [isGeneratingKeys, setIsGeneratingKeys] = React.useState(false);
const [isVerified, setIsVerified] = React.useState(false); const [isVerified, setIsVerified] = React.useState(false);
const [securityLevel, setSecurityLevel] = React.useState(null); const [securityLevel, setSecurityLevel] = React.useState(null);
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0); const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
@@ -1474,12 +1511,9 @@ var EnhancedSecureP2PChat = () => {
})); }));
}; };
const shouldPreserveAnswerData = () => { const shouldPreserveAnswerData = () => {
const now = Date.now(); const hasAnswerData = !!answerData || answerInput && typeof answerInput === "string" && answerInput.trim().length > 0;
const answerAge = now - (connectionState.answerCreatedAt || 0); const hasAnswerQR = qrCodeUrl && typeof qrCodeUrl === "string" && qrCodeUrl.trim().length > 0;
const maxPreserveTime = 3e5; const shouldPreserve = connectionState.hasActiveAnswer && !connectionState.isUserInitiatedDisconnect || hasAnswerData && !connectionState.isUserInitiatedDisconnect || hasAnswerQR && !connectionState.isUserInitiatedDisconnect;
const hasAnswerData = answerData && typeof answerData === "string" && answerData.trim().length > 0 || answerInput && answerInput.trim().length > 0;
const hasAnswerQR = qrCodeUrl && qrCodeUrl.trim().length > 0;
const shouldPreserve = connectionState.hasActiveAnswer && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerData && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerQR && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect;
return shouldPreserve; return shouldPreserve;
}; };
const markAnswerCreated = () => { const markAnswerCreated = () => {
@@ -1488,26 +1522,15 @@ var EnhancedSecureP2PChat = () => {
answerCreatedAt: Date.now() answerCreatedAt: Date.now()
}); });
}; };
React.useEffect(() => { const webrtcManagerRef = React.useRef(null);
window.forceCleanup = () => {
handleClearData();
if (webrtcManagerRef2.current) {
webrtcManagerRef2.current.disconnect();
}
};
window.clearLogs = () => {
if (typeof console.clear === "function") {
console.clear();
}
};
return () => {
delete window.forceCleanup;
delete window.clearLogs;
};
}, []);
const webrtcManagerRef2 = React.useRef(null);
const notificationIntegrationRef = React.useRef(null); const notificationIntegrationRef = React.useRef(null);
window.webrtcManagerRef = webrtcManagerRef2; React.useEffect(() => {
return installDebugWindowHooks({
targetWindow: window,
webrtcManagerRef,
onClearData: handleClearData
});
}, []);
const addMessageWithAutoScroll = React.useCallback((message, type) => { const addMessageWithAutoScroll = React.useCallback((message, type) => {
const newMessage = { const newMessage = {
message, message,
@@ -1548,7 +1571,7 @@ var EnhancedSecureP2PChat = () => {
} }
window.isUpdatingSecurity = true; window.isUpdatingSecurity = true;
try { try {
if (webrtcManagerRef2.current) { if (webrtcManagerRef.current) {
setSecurityLevel({ setSecurityLevel({
level: "MAXIMUM", level: "MAXIMUM",
score: 100, score: 100,
@@ -1559,7 +1582,7 @@ var EnhancedSecureP2PChat = () => {
isRealData: true isRealData: true
}); });
if (window.DEBUG_MODE) { if (window.DEBUG_MODE) {
const currentLevel = webrtcManagerRef2.current.ecdhKeyPair && webrtcManagerRef2.current.ecdsaKeyPair ? await webrtcManagerRef2.current.calculateSecurityLevel() : { const currentLevel = webrtcManagerRef.current.ecdhKeyPair && webrtcManagerRef.current.ecdsaKeyPair ? await webrtcManagerRef.current.calculateSecurityLevel() : {
level: "MAXIMUM", level: "MAXIMUM",
score: 100, score: 100,
sessionType: "premium", sessionType: "premium",
@@ -1589,8 +1612,8 @@ var EnhancedSecureP2PChat = () => {
localStorage.setItem("securebit_relay_only_mode", String(relayOnlyMode)); localStorage.setItem("securebit_relay_only_mode", String(relayOnlyMode));
} catch { } catch {
} }
if (webrtcManagerRef2.current?._config?.webrtc) { if (webrtcManagerRef.current?._config?.webrtc) {
webrtcManagerRef2.current._config.webrtc.relayOnly = relayOnlyMode; webrtcManagerRef.current._setRelayOnlyMode(relayOnlyMode);
} }
}, [relayOnlyMode]); }, [relayOnlyMode]);
React.useEffect(() => { React.useEffect(() => {
@@ -1601,7 +1624,7 @@ var EnhancedSecureP2PChat = () => {
} }
}, [messages]); }, [messages]);
React.useEffect(() => { React.useEffect(() => {
if (webrtcManagerRef2.current) { if (webrtcManagerRef.current) {
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping..."); console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
return; return;
} }
@@ -1763,7 +1786,7 @@ var EnhancedSecureP2PChat = () => {
if (typeof console.clear === "function") { if (typeof console.clear === "function") {
console.clear(); console.clear();
} }
webrtcManagerRef2.current = new EnhancedSecureWebRTCManager( webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
handleMessage, handleMessage,
handleStatusChange, handleStatusChange,
handleKeyExchange, handleKeyExchange,
@@ -1779,7 +1802,7 @@ var EnhancedSecureP2PChat = () => {
); );
if (typeof Notification !== "undefined" && Notification && Notification.permission === "granted" && window.NotificationIntegration && !notificationIntegrationRef.current) { if (typeof Notification !== "undefined" && Notification && Notification.permission === "granted" && window.NotificationIntegration && !notificationIntegrationRef.current) {
try { try {
const integration = new window.NotificationIntegration(webrtcManagerRef2.current); const integration = new window.NotificationIntegration(webrtcManagerRef.current);
integration.init().then(() => { integration.init().then(() => {
notificationIntegrationRef.current = integration; notificationIntegrationRef.current = integration;
}).catch((error) => { }).catch((error) => {
@@ -1787,12 +1810,12 @@ var EnhancedSecureP2PChat = () => {
} catch (error) { } catch (error) {
} }
} }
handleMessage(" SecureBit.chat Enhanced Security Edition v4.8.5 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system"); handleMessage(" SecureBit.chat Enhanced Security Edition v4.8.7 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
if (event.type === "beforeunload" && !isTabSwitching) { if (event.type === "beforeunload" && !isTabSwitching) {
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) { if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
try { try {
webrtcManagerRef2.current.sendSystemMessage({ webrtcManagerRef.current.sendSystemMessage({
type: "peer_disconnect", type: "peer_disconnect",
reason: "user_disconnect", reason: "user_disconnect",
timestamp: Date.now() timestamp: Date.now()
@@ -1800,12 +1823,12 @@ var EnhancedSecureP2PChat = () => {
} catch (error) { } catch (error) {
} }
setTimeout(() => { setTimeout(() => {
if (webrtcManagerRef2.current) { if (webrtcManagerRef.current) {
webrtcManagerRef2.current.disconnect(); webrtcManagerRef.current.disconnect();
} }
}, 100); }, 100);
} else if (webrtcManagerRef2.current) { } else if (webrtcManagerRef.current) {
webrtcManagerRef2.current.disconnect(); webrtcManagerRef.current.disconnect();
} }
} else if (isTabSwitching) { } else if (isTabSwitching) {
event.preventDefault(); event.preventDefault();
@@ -1833,8 +1856,8 @@ var EnhancedSecureP2PChat = () => {
} }
}; };
document.addEventListener("visibilitychange", handleVisibilityChange); document.addEventListener("visibilitychange", handleVisibilityChange);
if (webrtcManagerRef2.current) { if (webrtcManagerRef.current) {
webrtcManagerRef2.current.setFileTransferCallbacks( webrtcManagerRef.current.setFileTransferCallbacks(
// Progress callback // Progress callback
(progress) => { (progress) => {
console.log("File progress:", progress); console.log("File progress:", progress);
@@ -1886,9 +1909,9 @@ var EnhancedSecureP2PChat = () => {
clearTimeout(tabSwitchTimeout); clearTimeout(tabSwitchTimeout);
tabSwitchTimeout = null; tabSwitchTimeout = null;
} }
if (webrtcManagerRef2.current) { if (webrtcManagerRef.current) {
webrtcManagerRef2.current.disconnect(); webrtcManagerRef.current.disconnect();
webrtcManagerRef2.current = null; webrtcManagerRef.current = null;
} }
}; };
}, []); }, []);
@@ -2553,12 +2576,12 @@ var EnhancedSecureP2PChat = () => {
}; };
const handleCreateOffer = async () => { const handleCreateOffer = async () => {
try { try {
setIsGeneratingKeys2(true); setIsGeneratingKeys(true);
setOfferData(""); setOfferData("");
setShowOfferStep(false); setShowOfferStep(false);
setShowQRCode(false); setShowQRCode(false);
setQrCodeUrl(""); setQrCodeUrl("");
const offer = await webrtcManagerRef2.current.createSecureOffer(); const offer = await webrtcManagerRef.current.createSecureOffer();
setOfferData(offer); setOfferData(offer);
setShowOfferStep(true); setShowOfferStep(true);
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer; const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
@@ -2643,7 +2666,7 @@ var EnhancedSecureP2PChat = () => {
timestamp: Date.now() timestamp: Date.now()
}]); }]);
} finally { } finally {
setIsGeneratingKeys2(false); setIsGeneratingKeys(false);
} }
}; };
const handleCreateAnswer = async () => { const handleCreateAnswer = async () => {
@@ -2683,7 +2706,7 @@ var EnhancedSecureP2PChat = () => {
if (!isValidOfferType) { if (!isValidOfferType) {
throw new Error("Invalid invitation type. Expected offer or enhanced_secure_offer"); throw new Error("Invalid invitation type. Expected offer or enhanced_secure_offer");
} }
const answer = await webrtcManagerRef2.current.createSecureAnswer(offer); const answer = await webrtcManagerRef.current.createSecureAnswer(offer);
setAnswerData(answer); setAnswerData(answer);
setShowAnswerStep(true); setShowAnswerStep(true);
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer; const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
@@ -2740,10 +2763,8 @@ var EnhancedSecureP2PChat = () => {
} catch (e) { } catch (e) {
console.warn("Answer QR generation failed:", e); console.warn("Answer QR generation failed:", e);
} }
if (answerInput.trim().length > 0) { if (typeof markAnswerCreated === "function") {
if (typeof markAnswerCreated === "function") { markAnswerCreated();
markAnswerCreated();
}
} }
const existingResponseMessages = messages.filter( const existingResponseMessages = messages.filter(
(m) => m.type === "system" && (m.message.includes("Secure response created") || m.message.includes("Send the response")) (m) => m.type === "system" && (m.message.includes("Secure response created") || m.message.includes("Send the response"))
@@ -2821,7 +2842,7 @@ var EnhancedSecureP2PChat = () => {
if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") { if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") {
throw new Error("Invalid response type. Expected answer or enhanced_secure_answer"); throw new Error("Invalid response type. Expected answer or enhanced_secure_answer");
} }
await webrtcManagerRef2.current.handleSecureAnswer(answer); await webrtcManagerRef.current.handleSecureAnswer(answer);
if (pendingSession) { if (pendingSession) {
setPendingSession(null); setPendingSession(null);
setMessages((prev) => [...prev, { setMessages((prev) => [...prev, {
@@ -2909,11 +2930,11 @@ var EnhancedSecureP2PChat = () => {
}; };
const handleVerifyConnection = async (userCode, isValid = true) => { const handleVerifyConnection = async (userCode, isValid = true) => {
if (isValid) { if (isValid) {
webrtcManagerRef2.current.confirmVerification(userCode); webrtcManagerRef.current.confirmVerification(userCode);
setLocalVerificationConfirmed(true); setLocalVerificationConfirmed(true);
try { try {
if (window.NotificationIntegration && webrtcManagerRef2.current && !notificationIntegrationRef.current) { if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
const integration = new window.NotificationIntegration(webrtcManagerRef2.current); const integration = new window.NotificationIntegration(webrtcManagerRef.current);
await integration.init(); await integration.init();
notificationIntegrationRef.current = integration; notificationIntegrationRef.current = integration;
const status = integration.getStatus(); const status = integration.getStatus();
@@ -2971,15 +2992,15 @@ var EnhancedSecureP2PChat = () => {
if (!messageInput.trim()) { if (!messageInput.trim()) {
return; return;
} }
if (!webrtcManagerRef2.current) { if (!webrtcManagerRef.current) {
return; return;
} }
if (!webrtcManagerRef2.current.isConnected()) { if (!webrtcManagerRef.current.isConnected()) {
return; return;
} }
try { try {
addMessageWithAutoScroll(messageInput.trim(), "sent"); addMessageWithAutoScroll(messageInput.trim(), "sent");
await webrtcManagerRef2.current.sendMessage(messageInput); await webrtcManagerRef.current.sendMessage(messageInput);
setMessageInput(""); setMessageInput("");
} catch (error) { } catch (error) {
const msg = String(error?.message || error); const msg = String(error?.message || error);
@@ -2994,7 +3015,7 @@ var EnhancedSecureP2PChat = () => {
setOfferInput(""); setOfferInput("");
setAnswerInput(""); setAnswerInput("");
setShowOfferStep(false); setShowOfferStep(false);
setIsGeneratingKeys2(false); setIsGeneratingKeys(false);
if (!shouldPreserveAnswerData()) { if (!shouldPreserveAnswerData()) {
setShowAnswerStep(false); setShowAnswerStep(false);
} }
@@ -3030,8 +3051,8 @@ var EnhancedSecureP2PChat = () => {
status: "disconnected", status: "disconnected",
isUserInitiatedDisconnect: true isUserInitiatedDisconnect: true
}); });
if (webrtcManagerRef2.current) { if (webrtcManagerRef.current) {
webrtcManagerRef2.current.disconnect(); webrtcManagerRef.current.disconnect();
} }
if (notificationIntegrationRef.current) { if (notificationIntegrationRef.current) {
notificationIntegrationRef.current.cleanup(); notificationIntegrationRef.current.cleanup();
@@ -3052,7 +3073,7 @@ var EnhancedSecureP2PChat = () => {
setAnswerInput(""); setAnswerInput("");
setShowOfferStep(false); setShowOfferStep(false);
setShowAnswerStep(false); setShowAnswerStep(false);
setIsGeneratingKeys2(false); setIsGeneratingKeys(false);
setShowQRCode(false); setShowQRCode(false);
setQrCodeUrl(""); setQrCodeUrl("");
setShowQRScanner(false); setShowQRScanner(false);
@@ -3195,7 +3216,7 @@ var EnhancedSecureP2PChat = () => {
isConnected: isConnectedAndVerified, isConnected: isConnectedAndVerified,
securityLevel, securityLevel,
// sessionManager removed - all features enabled by default // sessionManager removed - all features enabled by default
webrtcManager: webrtcManagerRef2.current webrtcManager: webrtcManagerRef.current
}), }),
React.createElement( React.createElement(
"main", "main",
@@ -3215,7 +3236,7 @@ var EnhancedSecureP2PChat = () => {
isVerified, isVerified,
chatMessagesRef, chatMessagesRef,
scrollToBottom, scrollToBottom,
webrtcManager: webrtcManagerRef2.current webrtcManager: webrtcManagerRef.current
}); });
})() : React.createElement(EnhancedConnectionSetup, { })() : React.createElement(EnhancedConnectionSetup, {
onCreateOffer: handleCreateOffer, onCreateOffer: handleCreateOffer,
@@ -3255,9 +3276,11 @@ var EnhancedSecureP2PChat = () => {
markAnswerCreated, markAnswerCreated,
notificationIntegrationRef, notificationIntegrationRef,
isGeneratingKeys, isGeneratingKeys,
setIsGeneratingKeys,
handleCreateOffer, handleCreateOffer,
relayOnlyMode, relayOnlyMode,
setRelayOnlyMode setRelayOnlyMode,
webrtcManagerRef
}) })
), ),
// QR Scanner Modal // QR Scanner Modal
+4 -4
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+6 -5
View File
@@ -113,7 +113,7 @@
<!-- GitHub Pages SEO --> <!-- GitHub Pages SEO -->
<meta name="description" content="SecureBit.chat v4.4.18 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography"> <meta name="description" content="SecureBit.chat v4.8.7 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
<meta name="keywords" content="P2P messenger, ECDH, DTLS, SAS, encryption, WebRTC, privacy, ASN.1 validation, military-grade security, 18-layer defense, MITM protection, PFS"> <meta name="keywords" content="P2P messenger, ECDH, DTLS, SAS, encryption, WebRTC, privacy, ASN.1 validation, military-grade security, 18-layer defense, MITM protection, PFS">
<meta name="author" content="Volodymyr"> <meta name="author" content="Volodymyr">
<link rel="canonical" href="https://github.com/SecureBitChat/securebit-chat/"> <link rel="canonical" href="https://github.com/SecureBitChat/securebit-chat/">
@@ -131,6 +131,7 @@
<meta name="twitter:description" content="P2P messenger with military-grade cryptography"> <meta name="twitter:description" content="P2P messenger with military-grade cryptography">
<title>SecureBit.chat - Enhanced Security Edition</title> <title>SecureBit.chat - Enhanced Security Edition</title>
<script src="config/ice-servers.js"></script>
<script src="libs/react/react.production.min.js"></script> <script src="libs/react/react.production.min.js"></script>
<script src="libs/react-dom/react-dom.production.min.js"></script> <script src="libs/react-dom/react-dom.production.min.js"></script>
<link rel="stylesheet" href="assets/tailwind.css"> <link rel="stylesheet" href="assets/tailwind.css">
@@ -147,13 +148,13 @@
<!-- Update Manager - система принудительного обновления --> <!-- Update Manager - система принудительного обновления -->
<script src="src/utils/updateManager.js"></script> <script src="src/utils/updateManager.js"></script>
<script type="module" src="src/components/UpdateChecker.jsx"></script> <script type="module" src="src/components/UpdateChecker.jsx"></script>
<script type="module" src="dist/qr-local.js?v=1779043608721"></script> <script type="module" src="dist/qr-local.js?v=1779198538664"></script>
<script type="module" src="src/components/QRScanner.js?v=1779043608721"></script> <script type="module" src="src/components/QRScanner.js?v=1779198538664"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1779043608721"></script> <script type="module" src="dist/app-boot.js?v=1779198538664"></script>
<script type="module" src="dist/app.js?v=1779043608721"></script> <script type="module" src="dist/app.js?v=1779198538664"></script>
<script src="src/scripts/pwa-register.js"></script> <script src="src/scripts/pwa-register.js"></script>
<script src="./src/pwa/install-prompt.js" type="module"></script> <script src="./src/pwa/install-prompt.js" type="module"></script>
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "SecureBit.chat v4.8.6 - ECDH + DTLS + SAS", "name": "SecureBit.chat v4.8.7 - ECDH + DTLS + SAS",
"short_name": "SecureBit", "short_name": "SecureBit",
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments", "description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
"start_url": "./", "start_url": "./",
+7 -7
View File
@@ -1,10 +1,10 @@
{ {
"version": "1779043608721", "version": "1779198538664",
"buildVersion": "1779043608721", "buildVersion": "1779198538664",
"appVersion": "4.8.5", "appVersion": "4.8.7",
"buildTime": "2026-05-17T18:46:48.763Z", "buildTime": "2026-05-19T13:48:58.703Z",
"buildId": "1779043608721-4b8c882", "buildId": "1779198538664-1cc8732",
"gitHash": "4b8c882", "gitHash": "1cc8732",
"generated": true, "generated": true,
"generatedAt": "2026-05-17T18:46:48.764Z" "generatedAt": "2026-05-19T13:48:58.704Z"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "securebit-chat", "name": "securebit-chat",
"version": "4.8.6", "version": "4.8.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "securebit-chat", "name": "securebit-chat",
"version": "4.8.6", "version": "4.8.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"base64-js": "1.5.1", "base64-js": "1.5.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "securebit-chat", "name": "securebit-chat",
"version": "4.8.6", "version": "4.8.7",
"description": "Secure P2P Communication Application with End-to-End Encryption", "description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html", "main": "index.html",
"scripts": { "scripts": {
+1 -1
View File
@@ -2005,7 +2005,7 @@ import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
} }
} }
handleMessage(' SecureBit.chat Enhanced Security Edition v4.8.5 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system'); handleMessage(' SecureBit.chat Enhanced Security Edition v4.8.7 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
if (event.type === 'beforeunload' && !isTabSwitching) { if (event.type === 'beforeunload' && !isTabSwitching) {
+162 -9
View File
@@ -257,6 +257,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
this.sequenceNumber = 0; this.sequenceNumber = 0;
this.expectedSequenceNumber = 0; this.expectedSequenceNumber = 0;
this.sessionSalt = null; this.sessionSalt = null;
this._pendingOfferContext = null;
// Anti-Replay and Message Ordering Protection // Anti-Replay and Message Ordering Protection
this.replayWindowSize = 64; // Sliding window for replay protection this.replayWindowSize = 64; // Sliding window for replay protection
@@ -1023,6 +1024,83 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
return summary; return summary;
} }
_describeIceCandidatesInSDP(sdp) {
if (typeof sdp !== 'string') return [];
return (sdp.match(/^a=candidate:.*$/gm) || []).map((line) => {
const parts = line.slice('a=candidate:'.length).trim().split(/\s+/);
const typIndex = parts.findIndex(part => part.toLowerCase() === 'typ');
const address = parts[4] || '';
const port = parts[5] || '';
const candidateType = typIndex >= 0 ? parts[typIndex + 1] || 'unknown' : 'unknown';
const protocol = (parts[2] || 'unknown').toLowerCase();
let addressKind = 'unknown';
if (/\.local$/i.test(address)) {
addressKind = 'mdns';
} else if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[0-1])\.)/.test(address)) {
addressKind = 'private-ipv4';
} else if (/^\d{1,3}(\.\d{1,3}){3}$/.test(address)) {
addressKind = 'public-ipv4';
} else if (address.includes(':')) {
addressKind = 'ipv6';
}
return {
candidateType,
protocol,
addressKind,
portPresent: !!port,
tcpType: (() => {
const tcpIndex = parts.findIndex(part => part.toLowerCase() === 'tcptype');
return tcpIndex >= 0 ? parts[tcpIndex + 1] || null : null;
})()
};
});
}
_logIceCandidateDiagnostics(label, sdp, extra = {}) {
const candidateSummary = this._summarizeIceCandidatesInSDP(sdp);
const candidateDetails = this._describeIceCandidatesInSDP(sdp);
console.info(`[SecureBit ICE] ${label}`, {
candidateSummary,
candidateDetails,
candidateDetailsJson: JSON.stringify(candidateDetails),
...extra
});
return { candidateSummary, candidateDetails };
}
_hasOnlyMdnsHostCandidates(sdp) {
const summary = this._summarizeIceCandidatesInSDP(sdp);
const details = this._describeIceCandidatesInSDP(sdp);
return summary.total > 0 &&
summary.srflx === 0 &&
summary.relay === 0 &&
summary.prflx === 0 &&
details.every(candidate =>
candidate.candidateType === 'host' &&
candidate.addressKind === 'mdns'
);
}
_warnIfRemoteCandidatesNeedRelay(context, sdp) {
if (!this._hasOnlyMdnsHostCandidates(sdp)) return false;
const message = context === 'answer'
? 'Connection warning: the response contains only browser-masked mDNS host candidates and no server-reflexive or TURN relay candidates. This network/browser combination may not connect until TURN is configured.'
: 'Connection warning: the invitation contains only browser-masked mDNS host candidates and no server-reflexive or TURN relay candidates. This network/browser combination may not connect until TURN is configured.';
this._secureLog('warn', 'Remote ICE candidates require TURN or usable non-mDNS candidates', {
context,
candidateSummary: this._summarizeIceCandidatesInSDP(sdp),
candidateDetails: this._describeIceCandidatesInSDP(sdp)
});
this.deliverMessageToUI(message, 'system');
return true;
}
async _collectIceFailureDiagnostics() { async _collectIceFailureDiagnostics() {
if (!this.peerConnection?.getStats) return null; if (!this.peerConnection?.getStats) return null;
@@ -1073,6 +1151,50 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
} }
} }
_storePendingOfferContext() {
this._pendingOfferContext = {
sessionSalt: Array.isArray(this.sessionSalt) ? [...this.sessionSalt] : null,
sessionId: this.sessionId || null,
connectionId: this.connectionId || null,
keyFingerprint: this.keyFingerprint || null,
createdAt: Date.now()
};
}
_restorePendingOfferContextIfNeeded() {
const saltIsValid = Array.isArray(this.sessionSalt) && this.sessionSalt.length === 64;
if (saltIsValid) return true;
const pendingSalt = this._pendingOfferContext?.sessionSalt;
if (!Array.isArray(pendingSalt) || pendingSalt.length !== 64) {
return false;
}
this.sessionSalt = [...pendingSalt];
if (!this.sessionId && this._pendingOfferContext.sessionId) {
this.sessionId = this._pendingOfferContext.sessionId;
}
if (!this.connectionId && this._pendingOfferContext.connectionId) {
this.connectionId = this._pendingOfferContext.connectionId;
}
if (!this.keyFingerprint && this._pendingOfferContext.keyFingerprint) {
this.keyFingerprint = this._pendingOfferContext.keyFingerprint;
}
this._secureLog('warn', 'Restored pending offer context before applying answer', {
pendingContextAgeMs: Date.now() - (this._pendingOfferContext.createdAt || Date.now())
});
return true;
}
_clearPendingOfferContext() {
if (this._pendingOfferContext?.sessionSalt) {
this._secureWipeMemory(this._pendingOfferContext.sessionSalt, 'pendingOfferContext.sessionSalt');
}
this._pendingOfferContext = null;
}
/** /**
* Execute all maintenance tasks in a single cycle * Execute all maintenance tasks in a single cycle
*/ */
@@ -3067,6 +3189,8 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
this.connectionId = null; this.connectionId = null;
} }
this._clearPendingOfferContext();
this._secureLog('info', '🔒 Cryptographic materials securely cleaned up'); this._secureLog('info', '🔒 Cryptographic materials securely cleaned up');
} catch (error) { } catch (error) {
@@ -7389,6 +7513,33 @@ async processMessage(data) {
return config; return config;
} }
_summarizeIceServerConfig(iceServers = []) {
const summary = {
serverCount: 0,
stun: 0,
turn: 0,
turns: 0,
hasCredentials: false
};
for (const server of iceServers || []) {
summary.serverCount += 1;
if (server?.username || server?.credential) {
summary.hasCredentials = true;
}
const urls = Array.isArray(server?.urls) ? server.urls : [server?.urls];
for (const rawUrl of urls) {
const url = String(rawUrl || '').toLowerCase();
if (url.startsWith('stun:')) summary.stun += 1;
if (url.startsWith('turn:')) summary.turn += 1;
if (url.startsWith('turns:')) summary.turns += 1;
}
}
return summary;
}
_isRelayOnlyMode() { _isRelayOnlyMode() {
return this._config.webrtc.privacyMode === 'relay-only'; return this._config.webrtc.privacyMode === 'relay-only';
} }
@@ -7423,6 +7574,7 @@ async processMessage(data) {
this._sessionAlive = true; this._sessionAlive = true;
const config = this._buildPeerConnectionConfig(); const config = this._buildPeerConnectionConfig();
this._warnIfTurnMissing(); this._warnIfTurnMissing();
console.info('[SecureBit ICE] peer connection config', this._summarizeIceServerConfig(config.iceServers));
this.peerConnection = new RTCPeerConnection(config); this.peerConnection = new RTCPeerConnection(config);
@@ -9679,8 +9831,7 @@ async processMessage(data) {
iceGatheringDurationMs: Date.now() - offerIceGatheringStartedAt, iceGatheringDurationMs: Date.now() - offerIceGatheringStartedAt,
iceGatheringCompleted: offerIceGatheringCompleted iceGatheringCompleted: offerIceGatheringCompleted
}); });
console.info('[SecureBit ICE] offer export', { this._logIceCandidateDiagnostics('offer export', this.peerConnection.localDescription?.sdp, {
candidateSummary: offerCandidateSummary,
iceGatheringState: this.peerConnection.iceGatheringState, iceGatheringState: this.peerConnection.iceGatheringState,
iceGatheringDurationMs: Date.now() - offerIceGatheringStartedAt, iceGatheringDurationMs: Date.now() - offerIceGatheringStartedAt,
iceGatheringCompleted: offerIceGatheringCompleted iceGatheringCompleted: offerIceGatheringCompleted
@@ -9736,6 +9887,7 @@ async processMessage(data) {
// Generate connection ID for AAD // Generate connection ID for AAD
this.connectionId = Array.from(crypto.getRandomValues(new Uint8Array(8))) this.connectionId = Array.from(crypto.getRandomValues(new Uint8Array(8)))
.map(b => b.toString(16).padStart(2, '0')).join(''); .map(b => b.toString(16).padStart(2, '0')).join('');
this._storePendingOfferContext();
// ============================================ // ============================================
// PHASE 11: SECURITY LEVEL CALCULATION // PHASE 11: SECURITY LEVEL CALCULATION
@@ -10322,10 +10474,10 @@ async processMessage(data) {
type: 'offer', type: 'offer',
sdp: offerData.s || offerData.sdp sdp: offerData.s || offerData.sdp
})); }));
console.info('[SecureBit ICE] remote offer applied', { this._logIceCandidateDiagnostics('remote offer applied', this.peerConnection.remoteDescription?.sdp, {
candidateSummary: this._summarizeIceCandidatesInSDP(this.peerConnection.remoteDescription?.sdp),
signalingState: this.peerConnection.signalingState signalingState: this.peerConnection.signalingState
}); });
this._warnIfRemoteCandidatesNeedRelay('offer', this.peerConnection.remoteDescription?.sdp);
this._secureLog('debug', 'Remote description set successfully', { this._secureLog('debug', 'Remote description set successfully', {
operationId: operationId, operationId: operationId,
@@ -10418,8 +10570,7 @@ async processMessage(data) {
iceGatheringDurationMs: Date.now() - answerIceGatheringStartedAt, iceGatheringDurationMs: Date.now() - answerIceGatheringStartedAt,
iceGatheringCompleted: answerIceGatheringCompleted iceGatheringCompleted: answerIceGatheringCompleted
}); });
console.info('[SecureBit ICE] answer export', { this._logIceCandidateDiagnostics('answer export', this.peerConnection.localDescription?.sdp, {
candidateSummary: answerCandidateSummary,
iceGatheringState: this.peerConnection.iceGatheringState, iceGatheringState: this.peerConnection.iceGatheringState,
iceGatheringDurationMs: Date.now() - answerIceGatheringStartedAt, iceGatheringDurationMs: Date.now() - answerIceGatheringStartedAt,
iceGatheringCompleted: answerIceGatheringCompleted iceGatheringCompleted: answerIceGatheringCompleted
@@ -10676,6 +10827,7 @@ async processMessage(data) {
_cleanupFailedAnswerCreation() { _cleanupFailedAnswerCreation() {
try { try {
// Secure wipe of cryptographic materials // Secure wipe of cryptographic materials
this._clearPendingOfferContext();
this._secureCleanupCryptographicMaterials(); this._secureCleanupCryptographicMaterials();
// Secure wipe of PFS key versions // Secure wipe of PFS key versions
@@ -10930,11 +11082,12 @@ async processMessage(data) {
); );
// Additional MITM protection: Verify session salt integrity // Additional MITM protection: Verify session salt integrity
this._restorePendingOfferContextIfNeeded();
if (!this.sessionSalt || this.sessionSalt.length !== 64) { if (!this.sessionSalt || this.sessionSalt.length !== 64) {
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid session salt detected - possible session hijacking', { window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid session salt detected - possible session hijacking', {
saltLength: this.sessionSalt ? this.sessionSalt.length : 0 saltLength: this.sessionSalt ? this.sessionSalt.length : 0
}); });
throw new Error('Invalid session salt possible session hijacking attempt'); throw new Error('Missing pending offer context. Apply the response in the original creator window that generated the invitation.');
} }
// Verify that the session salt hasn't been tampered with // Verify that the session salt hasn't been tampered with
@@ -11101,10 +11254,10 @@ async processMessage(data) {
type: 'answer', type: 'answer',
sdp: sdpData sdp: sdpData
}); });
console.info('[SecureBit ICE] remote answer applied', { this._logIceCandidateDiagnostics('remote answer applied', this.peerConnection.remoteDescription?.sdp, {
candidateSummary: this._summarizeIceCandidatesInSDP(this.peerConnection.remoteDescription?.sdp),
signalingState: this.peerConnection.signalingState signalingState: this.peerConnection.signalingState
}); });
this._warnIfRemoteCandidatesNeedRelay('answer', this.peerConnection.remoteDescription?.sdp);
this._secureLog('debug', 'Remote description set successfully from answer', { this._secureLog('debug', 'Remote description set successfully from answer', {
signalingState: this.peerConnection.signalingState signalingState: this.peerConnection.signalingState
+99
View File
@@ -333,6 +333,105 @@ function createVerificationReadinessManager({
); );
} }
// ICE candidate details are redacted but still expose routing-relevant classes.
{
const sdp = [
'v=0',
'a=candidate:1 1 UDP 2122252543 192.168.1.2 54400 typ host',
'a=candidate:2 1 UDP 1686052607 203.0.113.10 40000 typ srflx',
'a=candidate:3 1 TCP 1518280447 abcdef.local 9 typ host tcptype passive'
].join('\r\n');
assert.deepEqual(
EnhancedSecureWebRTCManager.prototype._describeIceCandidatesInSDP.call(createSASManager(), sdp),
[
{ candidateType: 'host', protocol: 'udp', addressKind: 'private-ipv4', portPresent: true, tcpType: null },
{ candidateType: 'srflx', protocol: 'udp', addressKind: 'public-ipv4', portPresent: true, tcpType: null },
{ candidateType: 'host', protocol: 'tcp', addressKind: 'mdns', portPresent: true, tcpType: 'passive' }
]
);
}
// ICE diagnostics include a copyable JSON string so browser logs do not hide
// candidate details behind collapsed DevTools objects.
{
const logs = [];
const originalConsoleInfo = console.info;
console.info = (...args) => logs.push(args);
try {
const manager = {
_summarizeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP,
_describeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._describeIceCandidatesInSDP,
_logIceCandidateDiagnostics: EnhancedSecureWebRTCManager.prototype._logIceCandidateDiagnostics
};
manager._logIceCandidateDiagnostics('test candidates', 'a=candidate:1 1 UDP 1 192.168.1.2 5000 typ host', {
signalingState: 'stable'
});
assert.equal(logs[0][0], '[SecureBit ICE] test candidates');
assert.equal(typeof logs[0][1].candidateDetailsJson, 'string');
assert.match(logs[0][1].candidateDetailsJson, /private-ipv4/);
assert.equal(logs[0][1].signalingState, 'stable');
} finally {
console.info = originalConsoleInfo;
}
}
// Remote mDNS-only candidates are surfaced as a user-visible TURN warning.
{
const messages = [];
const manager = {
_secureLog() {},
deliverMessageToUI(message, type) {
messages.push({ message, type });
},
_summarizeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP,
_describeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._describeIceCandidatesInSDP,
_hasOnlyMdnsHostCandidates: EnhancedSecureWebRTCManager.prototype._hasOnlyMdnsHostCandidates,
_warnIfRemoteCandidatesNeedRelay: EnhancedSecureWebRTCManager.prototype._warnIfRemoteCandidatesNeedRelay
};
const mdnsOnlySdp = 'a=candidate:1 1 UDP 1 abcdef.local 5000 typ host';
const srflxSdp = 'a=candidate:1 1 UDP 1 203.0.113.10 5000 typ srflx';
assert.equal(manager._hasOnlyMdnsHostCandidates(mdnsOnlySdp), true);
assert.equal(manager._warnIfRemoteCandidatesNeedRelay('answer', mdnsOnlySdp), true);
assert.equal(messages[0].type, 'system');
assert.match(messages[0].message, /TURN is configured/i);
assert.equal(manager._hasOnlyMdnsHostCandidates(srflxSdp), false);
}
// Pending offer context preserves the creator's manual-exchange salt until the
// answer is applied, even if transient ICE/UI state temporarily loses it.
{
const manager = {
sessionSalt: Array.from({ length: 64 }, (_, index) => index),
sessionId: 'session-a',
connectionId: 'connection-a',
keyFingerprint: 'AA:BB',
_secureLog() {},
_secureWipeMemory() {},
_storePendingOfferContext: EnhancedSecureWebRTCManager.prototype._storePendingOfferContext,
_restorePendingOfferContextIfNeeded: EnhancedSecureWebRTCManager.prototype._restorePendingOfferContextIfNeeded,
_clearPendingOfferContext: EnhancedSecureWebRTCManager.prototype._clearPendingOfferContext
};
manager._storePendingOfferContext();
manager.sessionSalt = null;
manager.sessionId = null;
manager.connectionId = null;
manager.keyFingerprint = null;
assert.equal(manager._restorePendingOfferContextIfNeeded(), true);
assert.equal(manager.sessionSalt.length, 64);
assert.equal(manager.sessionId, 'session-a');
assert.equal(manager.connectionId, 'connection-a');
assert.equal(manager.keyFingerprint, 'AA:BB');
manager._clearPendingOfferContext();
manager.sessionSalt = null;
assert.equal(manager._restorePendingOfferContextIfNeeded(), false);
}
// Joining with an offer and generating an answer does not open verification // Joining with an offer and generating an answer does not open verification
// before the answer has been applied by the creator and the channel opens. // before the answer has been applied by the creator and the channel opens.
{ {
+26
View File
@@ -112,4 +112,30 @@ function fake(config = {}) {
assert.equal(config.iceServers, overrideServers); assert.equal(config.iceServers, overrideServers);
} }
// ICE config diagnostics reveal whether TURN credentials were loaded without
// printing sensitive usernames or passwords.
{
const manager = fake({
iceServers: [
{ urls: 'stun:stun.example.test:3478' },
{
urls: ['turn:turn.example.test:3478?transport=udp', 'turns:turn.example.test:443?transport=tcp'],
username: 'user',
credential: 'secret'
}
]
});
const summary = EnhancedSecureWebRTCManager.prototype._summarizeIceServerConfig.call(
manager,
manager._config.webrtc.iceServers
);
assert.deepEqual(summary, {
serverCount: 2,
stun: 1,
turn: 1,
turns: 1,
hasCredentials: true
});
}
console.log('WebRTC privacy mode tests passed'); console.log('WebRTC privacy mode tests passed');