release: v4.8.7 WebRTC join reliability patch
This commit is contained in:
+21
-2
@@ -1,6 +1,25 @@
|
|||||||
# Changelog
|
# 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// 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
|
||||||
|
|||||||
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 -->
|
<!-- 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
@@ -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": "./",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user