release: v4.8.7 WebRTC join reliability patch
This commit is contained in:
+21
-2
@@ -1,6 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## v4.8.6 — Security hardening patch release
|
||||
## v4.8.7 — WebRTC 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.
|
||||
|
||||
@@ -29,7 +48,7 @@ This patch release strengthens SecureBit.chat across verification, sanitization,
|
||||
- Clean install succeeds with `npm ci`.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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:
|
||||
|
||||
|
||||
@@ -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='
|
||||
}
|
||||
];
|
||||
Vendored
+1646
-110
File diff suppressed because it is too large
Load Diff
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+91
-68
@@ -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
|
||||
var EnhancedCopyButton = ({ text, className = "", children }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
@@ -322,9 +355,11 @@ var EnhancedConnectionSetup = ({
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef,
|
||||
isGeneratingKeys,
|
||||
setIsGeneratingKeys,
|
||||
handleCreateOffer,
|
||||
relayOnlyMode,
|
||||
setRelayOnlyMode
|
||||
setRelayOnlyMode,
|
||||
webrtcManagerRef
|
||||
}) => {
|
||||
const [mode, setMode] = React.useState("select");
|
||||
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||
@@ -714,6 +749,7 @@ var EnhancedConnectionSetup = ({
|
||||
text: (() => {
|
||||
try {
|
||||
const min = typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
|
||||
if (!min) return "";
|
||||
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||
return window.encodeBinaryToPrefixed(min);
|
||||
}
|
||||
@@ -1032,6 +1068,7 @@ var EnhancedConnectionSetup = ({
|
||||
text: (() => {
|
||||
try {
|
||||
const min = typeof answerData === "object" ? JSON.stringify(answerData) : answerData || "";
|
||||
if (!min) return "";
|
||||
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||
return window.encodeBinaryToPrefixed(min);
|
||||
}
|
||||
@@ -1449,7 +1486,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
const [qrCodeUrl, setQrCodeUrl] = React.useState("");
|
||||
const [showQRScanner, setShowQRScanner] = 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 [securityLevel, setSecurityLevel] = React.useState(null);
|
||||
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
||||
@@ -1474,12 +1511,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
}));
|
||||
};
|
||||
const shouldPreserveAnswerData = () => {
|
||||
const now = Date.now();
|
||||
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
||||
const maxPreserveTime = 3e5;
|
||||
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;
|
||||
const hasAnswerData = !!answerData || answerInput && typeof answerInput === "string" && answerInput.trim().length > 0;
|
||||
const hasAnswerQR = qrCodeUrl && typeof qrCodeUrl === "string" && qrCodeUrl.trim().length > 0;
|
||||
const shouldPreserve = connectionState.hasActiveAnswer && !connectionState.isUserInitiatedDisconnect || hasAnswerData && !connectionState.isUserInitiatedDisconnect || hasAnswerQR && !connectionState.isUserInitiatedDisconnect;
|
||||
return shouldPreserve;
|
||||
};
|
||||
const markAnswerCreated = () => {
|
||||
@@ -1488,26 +1522,15 @@ var EnhancedSecureP2PChat = () => {
|
||||
answerCreatedAt: Date.now()
|
||||
});
|
||||
};
|
||||
React.useEffect(() => {
|
||||
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 webrtcManagerRef = 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 newMessage = {
|
||||
message,
|
||||
@@ -1548,7 +1571,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
window.isUpdatingSecurity = true;
|
||||
try {
|
||||
if (webrtcManagerRef2.current) {
|
||||
if (webrtcManagerRef.current) {
|
||||
setSecurityLevel({
|
||||
level: "MAXIMUM",
|
||||
score: 100,
|
||||
@@ -1559,7 +1582,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isRealData: true
|
||||
});
|
||||
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",
|
||||
score: 100,
|
||||
sessionType: "premium",
|
||||
@@ -1589,8 +1612,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
localStorage.setItem("securebit_relay_only_mode", String(relayOnlyMode));
|
||||
} catch {
|
||||
}
|
||||
if (webrtcManagerRef2.current?._config?.webrtc) {
|
||||
webrtcManagerRef2.current._config.webrtc.relayOnly = relayOnlyMode;
|
||||
if (webrtcManagerRef.current?._config?.webrtc) {
|
||||
webrtcManagerRef.current._setRelayOnlyMode(relayOnlyMode);
|
||||
}
|
||||
}, [relayOnlyMode]);
|
||||
React.useEffect(() => {
|
||||
@@ -1601,7 +1624,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
}, [messages]);
|
||||
React.useEffect(() => {
|
||||
if (webrtcManagerRef2.current) {
|
||||
if (webrtcManagerRef.current) {
|
||||
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
@@ -1763,7 +1786,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (typeof console.clear === "function") {
|
||||
console.clear();
|
||||
}
|
||||
webrtcManagerRef2.current = new EnhancedSecureWebRTCManager(
|
||||
webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
|
||||
handleMessage,
|
||||
handleStatusChange,
|
||||
handleKeyExchange,
|
||||
@@ -1779,7 +1802,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
);
|
||||
if (typeof Notification !== "undefined" && Notification && Notification.permission === "granted" && window.NotificationIntegration && !notificationIntegrationRef.current) {
|
||||
try {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef2.current);
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
integration.init().then(() => {
|
||||
notificationIntegrationRef.current = integration;
|
||||
}).catch((error) => {
|
||||
@@ -1787,12 +1810,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
} 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) => {
|
||||
if (event.type === "beforeunload" && !isTabSwitching) {
|
||||
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
|
||||
if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
|
||||
try {
|
||||
webrtcManagerRef2.current.sendSystemMessage({
|
||||
webrtcManagerRef.current.sendSystemMessage({
|
||||
type: "peer_disconnect",
|
||||
reason: "user_disconnect",
|
||||
timestamp: Date.now()
|
||||
@@ -1800,12 +1823,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
}
|
||||
}, 100);
|
||||
} else if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
} else if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
}
|
||||
} else if (isTabSwitching) {
|
||||
event.preventDefault();
|
||||
@@ -1833,8 +1856,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.setFileTransferCallbacks(
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.setFileTransferCallbacks(
|
||||
// Progress callback
|
||||
(progress) => {
|
||||
console.log("File progress:", progress);
|
||||
@@ -1886,9 +1909,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
clearTimeout(tabSwitchTimeout);
|
||||
tabSwitchTimeout = null;
|
||||
}
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
webrtcManagerRef2.current = null;
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
webrtcManagerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -2553,12 +2576,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
};
|
||||
const handleCreateOffer = async () => {
|
||||
try {
|
||||
setIsGeneratingKeys2(true);
|
||||
setIsGeneratingKeys(true);
|
||||
setOfferData("");
|
||||
setShowOfferStep(false);
|
||||
setShowQRCode(false);
|
||||
setQrCodeUrl("");
|
||||
const offer = await webrtcManagerRef2.current.createSecureOffer();
|
||||
const offer = await webrtcManagerRef.current.createSecureOffer();
|
||||
setOfferData(offer);
|
||||
setShowOfferStep(true);
|
||||
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
||||
@@ -2643,7 +2666,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} finally {
|
||||
setIsGeneratingKeys2(false);
|
||||
setIsGeneratingKeys(false);
|
||||
}
|
||||
};
|
||||
const handleCreateAnswer = async () => {
|
||||
@@ -2683,7 +2706,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!isValidOfferType) {
|
||||
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);
|
||||
setShowAnswerStep(true);
|
||||
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
||||
@@ -2740,10 +2763,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (e) {
|
||||
console.warn("Answer QR generation failed:", e);
|
||||
}
|
||||
if (answerInput.trim().length > 0) {
|
||||
if (typeof markAnswerCreated === "function") {
|
||||
markAnswerCreated();
|
||||
}
|
||||
if (typeof markAnswerCreated === "function") {
|
||||
markAnswerCreated();
|
||||
}
|
||||
const existingResponseMessages = messages.filter(
|
||||
(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") {
|
||||
throw new Error("Invalid response type. Expected answer or enhanced_secure_answer");
|
||||
}
|
||||
await webrtcManagerRef2.current.handleSecureAnswer(answer);
|
||||
await webrtcManagerRef.current.handleSecureAnswer(answer);
|
||||
if (pendingSession) {
|
||||
setPendingSession(null);
|
||||
setMessages((prev) => [...prev, {
|
||||
@@ -2909,11 +2930,11 @@ var EnhancedSecureP2PChat = () => {
|
||||
};
|
||||
const handleVerifyConnection = async (userCode, isValid = true) => {
|
||||
if (isValid) {
|
||||
webrtcManagerRef2.current.confirmVerification(userCode);
|
||||
webrtcManagerRef.current.confirmVerification(userCode);
|
||||
setLocalVerificationConfirmed(true);
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef2.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef2.current);
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
const status = integration.getStatus();
|
||||
@@ -2971,15 +2992,15 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!messageInput.trim()) {
|
||||
return;
|
||||
}
|
||||
if (!webrtcManagerRef2.current) {
|
||||
if (!webrtcManagerRef.current) {
|
||||
return;
|
||||
}
|
||||
if (!webrtcManagerRef2.current.isConnected()) {
|
||||
if (!webrtcManagerRef.current.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
addMessageWithAutoScroll(messageInput.trim(), "sent");
|
||||
await webrtcManagerRef2.current.sendMessage(messageInput);
|
||||
await webrtcManagerRef.current.sendMessage(messageInput);
|
||||
setMessageInput("");
|
||||
} catch (error) {
|
||||
const msg = String(error?.message || error);
|
||||
@@ -2994,7 +3015,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
setIsGeneratingKeys2(false);
|
||||
setIsGeneratingKeys(false);
|
||||
if (!shouldPreserveAnswerData()) {
|
||||
setShowAnswerStep(false);
|
||||
}
|
||||
@@ -3030,8 +3051,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
status: "disconnected",
|
||||
isUserInitiatedDisconnect: true
|
||||
});
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
}
|
||||
if (notificationIntegrationRef.current) {
|
||||
notificationIntegrationRef.current.cleanup();
|
||||
@@ -3052,7 +3073,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
setShowAnswerStep(false);
|
||||
setIsGeneratingKeys2(false);
|
||||
setIsGeneratingKeys(false);
|
||||
setShowQRCode(false);
|
||||
setQrCodeUrl("");
|
||||
setShowQRScanner(false);
|
||||
@@ -3195,7 +3216,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isConnected: isConnectedAndVerified,
|
||||
securityLevel,
|
||||
// sessionManager removed - all features enabled by default
|
||||
webrtcManager: webrtcManagerRef2.current
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
}),
|
||||
React.createElement(
|
||||
"main",
|
||||
@@ -3215,7 +3236,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isVerified,
|
||||
chatMessagesRef,
|
||||
scrollToBottom,
|
||||
webrtcManager: webrtcManagerRef2.current
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
});
|
||||
})() : React.createElement(EnhancedConnectionSetup, {
|
||||
onCreateOffer: handleCreateOffer,
|
||||
@@ -3255,9 +3276,11 @@ var EnhancedSecureP2PChat = () => {
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef,
|
||||
isGeneratingKeys,
|
||||
setIsGeneratingKeys,
|
||||
handleCreateOffer,
|
||||
relayOnlyMode,
|
||||
setRelayOnlyMode
|
||||
setRelayOnlyMode,
|
||||
webrtcManagerRef
|
||||
})
|
||||
),
|
||||
// QR Scanner Modal
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+6
-5
@@ -113,7 +113,7 @@
|
||||
|
||||
|
||||
<!-- 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="author" content="Volodymyr">
|
||||
<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">
|
||||
|
||||
<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-dom/react-dom.production.min.js"></script>
|
||||
<link rel="stylesheet" href="assets/tailwind.css">
|
||||
@@ -147,13 +148,13 @@
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script src="src/utils/updateManager.js"></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="src/components/QRScanner.js?v=1779043608721"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1779198538664"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1779198538664"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1779043608721"></script>
|
||||
<script type="module" src="dist/app.js?v=1779043608721"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1779198538664"></script>
|
||||
<script type="module" src="dist/app.js?v=1779198538664"></script>
|
||||
|
||||
<script src="src/scripts/pwa-register.js"></script>
|
||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||
|
||||
+1
-1
@@ -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",
|
||||
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
|
||||
"start_url": "./",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1779043608721",
|
||||
"buildVersion": "1779043608721",
|
||||
"appVersion": "4.8.5",
|
||||
"buildTime": "2026-05-17T18:46:48.763Z",
|
||||
"buildId": "1779043608721-4b8c882",
|
||||
"gitHash": "4b8c882",
|
||||
"version": "1779198538664",
|
||||
"buildVersion": "1779198538664",
|
||||
"appVersion": "4.8.7",
|
||||
"buildTime": "2026-05-19T13:48:58.703Z",
|
||||
"buildId": "1779198538664-1cc8732",
|
||||
"gitHash": "1cc8732",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-05-17T18:46:48.764Z"
|
||||
"generatedAt": "2026-05-19T13:48:58.704Z"
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "4.8.6",
|
||||
"version": "4.8.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "securebit-chat",
|
||||
"version": "4.8.6",
|
||||
"version": "4.8.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "1.5.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "4.8.6",
|
||||
"version": "4.8.7",
|
||||
"description": "Secure P2P Communication Application with End-to-End Encryption",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -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) => {
|
||||
if (event.type === 'beforeunload' && !isTabSwitching) {
|
||||
|
||||
@@ -257,6 +257,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
this.sequenceNumber = 0;
|
||||
this.expectedSequenceNumber = 0;
|
||||
this.sessionSalt = null;
|
||||
this._pendingOfferContext = null;
|
||||
|
||||
// Anti-Replay and Message Ordering 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;
|
||||
}
|
||||
|
||||
_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() {
|
||||
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
|
||||
*/
|
||||
@@ -3066,6 +3188,8 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
this._secureWipeMemory(this.connectionId, 'connectionId');
|
||||
this.connectionId = null;
|
||||
}
|
||||
|
||||
this._clearPendingOfferContext();
|
||||
|
||||
this._secureLog('info', '🔒 Cryptographic materials securely cleaned up');
|
||||
|
||||
@@ -7389,6 +7513,33 @@ async processMessage(data) {
|
||||
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() {
|
||||
return this._config.webrtc.privacyMode === 'relay-only';
|
||||
}
|
||||
@@ -7423,6 +7574,7 @@ async processMessage(data) {
|
||||
this._sessionAlive = true;
|
||||
const config = this._buildPeerConnectionConfig();
|
||||
this._warnIfTurnMissing();
|
||||
console.info('[SecureBit ICE] peer connection config', this._summarizeIceServerConfig(config.iceServers));
|
||||
|
||||
this.peerConnection = new RTCPeerConnection(config);
|
||||
|
||||
@@ -9679,8 +9831,7 @@ async processMessage(data) {
|
||||
iceGatheringDurationMs: Date.now() - offerIceGatheringStartedAt,
|
||||
iceGatheringCompleted: offerIceGatheringCompleted
|
||||
});
|
||||
console.info('[SecureBit ICE] offer export', {
|
||||
candidateSummary: offerCandidateSummary,
|
||||
this._logIceCandidateDiagnostics('offer export', this.peerConnection.localDescription?.sdp, {
|
||||
iceGatheringState: this.peerConnection.iceGatheringState,
|
||||
iceGatheringDurationMs: Date.now() - offerIceGatheringStartedAt,
|
||||
iceGatheringCompleted: offerIceGatheringCompleted
|
||||
@@ -9736,6 +9887,7 @@ async processMessage(data) {
|
||||
// Generate connection ID for AAD
|
||||
this.connectionId = Array.from(crypto.getRandomValues(new Uint8Array(8)))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
this._storePendingOfferContext();
|
||||
|
||||
// ============================================
|
||||
// PHASE 11: SECURITY LEVEL CALCULATION
|
||||
@@ -10322,10 +10474,10 @@ async processMessage(data) {
|
||||
type: 'offer',
|
||||
sdp: offerData.s || offerData.sdp
|
||||
}));
|
||||
console.info('[SecureBit ICE] remote offer applied', {
|
||||
candidateSummary: this._summarizeIceCandidatesInSDP(this.peerConnection.remoteDescription?.sdp),
|
||||
this._logIceCandidateDiagnostics('remote offer applied', this.peerConnection.remoteDescription?.sdp, {
|
||||
signalingState: this.peerConnection.signalingState
|
||||
});
|
||||
this._warnIfRemoteCandidatesNeedRelay('offer', this.peerConnection.remoteDescription?.sdp);
|
||||
|
||||
this._secureLog('debug', 'Remote description set successfully', {
|
||||
operationId: operationId,
|
||||
@@ -10418,8 +10570,7 @@ async processMessage(data) {
|
||||
iceGatheringDurationMs: Date.now() - answerIceGatheringStartedAt,
|
||||
iceGatheringCompleted: answerIceGatheringCompleted
|
||||
});
|
||||
console.info('[SecureBit ICE] answer export', {
|
||||
candidateSummary: answerCandidateSummary,
|
||||
this._logIceCandidateDiagnostics('answer export', this.peerConnection.localDescription?.sdp, {
|
||||
iceGatheringState: this.peerConnection.iceGatheringState,
|
||||
iceGatheringDurationMs: Date.now() - answerIceGatheringStartedAt,
|
||||
iceGatheringCompleted: answerIceGatheringCompleted
|
||||
@@ -10676,6 +10827,7 @@ async processMessage(data) {
|
||||
_cleanupFailedAnswerCreation() {
|
||||
try {
|
||||
// Secure wipe of cryptographic materials
|
||||
this._clearPendingOfferContext();
|
||||
this._secureCleanupCryptographicMaterials();
|
||||
|
||||
// Secure wipe of PFS key versions
|
||||
@@ -10930,11 +11082,12 @@ async processMessage(data) {
|
||||
);
|
||||
|
||||
// Additional MITM protection: Verify session salt integrity
|
||||
this._restorePendingOfferContextIfNeeded();
|
||||
if (!this.sessionSalt || this.sessionSalt.length !== 64) {
|
||||
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid session salt detected - possible session hijacking', {
|
||||
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
|
||||
@@ -11101,10 +11254,10 @@ async processMessage(data) {
|
||||
type: 'answer',
|
||||
sdp: sdpData
|
||||
});
|
||||
console.info('[SecureBit ICE] remote answer applied', {
|
||||
candidateSummary: this._summarizeIceCandidatesInSDP(this.peerConnection.remoteDescription?.sdp),
|
||||
this._logIceCandidateDiagnostics('remote answer applied', this.peerConnection.remoteDescription?.sdp, {
|
||||
signalingState: this.peerConnection.signalingState
|
||||
});
|
||||
this._warnIfRemoteCandidatesNeedRelay('answer', this.peerConnection.remoteDescription?.sdp);
|
||||
|
||||
this._secureLog('debug', 'Remote description set successfully from answer', {
|
||||
signalingState: this.peerConnection.signalingState
|
||||
|
||||
@@ -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
|
||||
// before the answer has been applied by the creator and the channel opens.
|
||||
{
|
||||
|
||||
@@ -112,4 +112,30 @@ function fake(config = {}) {
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user