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
## 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.
@@ -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.
+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.
@@ -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:
+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
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
+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 -->
<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
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",
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
"start_url": "./",
+7 -7
View File
@@ -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"
}
+2 -2
View File
@@ -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
View File
@@ -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
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) => {
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.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
+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
// 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);
}
// 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');