From cc7f850e7df87d5ec9b782d8c24ca888550769da Mon Sep 17 00:00:00 2001 From: lockbitchat Date: Sun, 17 May 2026 17:46:15 -0400 Subject: [PATCH] fix: bind SAS verification to DTLS fingerprint strings --- src/network/EnhancedSecureWebRTCManager.js | 50 ++++++++---- tests/sas-verification.test.mjs | 88 ++++++++++++++++++++++ 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js index 7151111..89588e7 100644 --- a/src/network/EnhancedSecureWebRTCManager.js +++ b/src/network/EnhancedSecureWebRTCManager.js @@ -3526,7 +3526,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida while ((match = fingerprintRegex.exec(sdp)) !== null) { fingerprints.push({ algorithm: match[1].toLowerCase(), - fingerprint: match[2].toLowerCase().replace(/:/g, '') + fingerprint: match[2].trim() }); } @@ -3536,7 +3536,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida while ((match = altFingerprintRegex.exec(sdp)) !== null) { fingerprints.push({ algorithm: match[1].toLowerCase(), - fingerprint: match[2].toLowerCase().replace(/:/g, '') + fingerprint: match[2].trim() }); } } @@ -3549,14 +3549,24 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida throw new Error('No DTLS fingerprints found in SDP'); } - // Prefer SHA-256 fingerprints - const sha256Fingerprint = fingerprints.find(fp => fp.algorithm === 'sha-256'); - if (sha256Fingerprint) { - return sha256Fingerprint.fingerprint; - } + // Prefer SHA-256, then choose lexicographically so multiple SDP + // fingerprint lines resolve to the same deterministic primary value. + const primaryFingerprint = [...fingerprints].sort((a, b) => { + const aIsSha256 = a.algorithm === 'sha-256'; + const bIsSha256 = b.algorithm === 'sha-256'; + if (aIsSha256 !== bIsSha256) { + return aIsSha256 ? -1 : 1; + } - // Fallback to first available fingerprint - return fingerprints[0].fingerprint; + const algorithmComparison = a.algorithm.localeCompare(b.algorithm); + if (algorithmComparison !== 0) { + return algorithmComparison; + } + + return a.fingerprint.localeCompare(b.fingerprint); + })[0]; + + return primaryFingerprint.fingerprint; } catch (error) { this._secureLog('error', 'Failed to extract DTLS fingerprint from SDP', { error: error.message, @@ -3625,18 +3635,28 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida async _computeSAS(keyMaterialRaw, localFP, remoteFP) { try { - if (!keyMaterialRaw || !localFP || !remoteFP) { + if (!keyMaterialRaw) { const missing = []; if (!keyMaterialRaw) missing.push('keyMaterialRaw'); - if (!localFP) missing.push('localFP'); - if (!remoteFP) missing.push('remoteFP'); throw new Error(`Missing required parameters for SAS computation: ${missing.join(', ')}`); } const enc = new TextEncoder(); + const normalizeFingerprintForSAS = (fingerprint, label) => { + if (typeof fingerprint !== 'string' || fingerprint.trim().length === 0) { + throw new Error( + `Security error: ${label} must be a non-empty DTLS fingerprint string for SAS computation` + ); + } + + return fingerprint.trim().toLowerCase(); + }; + + const normalizedLocalFP = normalizeFingerprintForSAS(localFP, 'localFP'); + const normalizedRemoteFP = normalizeFingerprintForSAS(remoteFP, 'remoteFP'); const salt = enc.encode( - 'webrtc-sas|' + [localFP, remoteFP].sort().join('|') + 'webrtc-sas|' + [normalizedLocalFP, normalizedRemoteFP].sort().join('|') ); let keyBuffer; @@ -3682,8 +3702,8 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida this._secureLog('info', 'SAS code computed successfully', { - localFP: localFP.substring(0, 16) + '...', - remoteFP: remoteFP.substring(0, 16) + '...', + localFP: normalizedLocalFP.substring(0, 16) + '...', + remoteFP: normalizedRemoteFP.substring(0, 16) + '...', sasLength: sasCode.length, timestamp: Date.now() }); diff --git a/tests/sas-verification.test.mjs b/tests/sas-verification.test.mjs index c3d8546..aa812fc 100644 --- a/tests/sas-verification.test.mjs +++ b/tests/sas-verification.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { webcrypto } from 'node:crypto'; let compareCalls = 0; globalThis.window = { @@ -38,6 +39,12 @@ function createFakeManager() { }; } +function createSASManager() { + return { + _secureLog() {} + }; +} + // testSASNormalization { const manager = createFakeManager(); @@ -84,4 +91,85 @@ function createFakeManager() { assert.equal(validManager.sent[0].data.verificationMethod, 'MANUAL_SAS_ENTRY'); } +// SAS is deterministic for the same key material and normalized fingerprints, +// and changes when either fingerprint changes. +{ + const manager = createSASManager(); + const keyMaterial = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const computeSAS = EnhancedSecureWebRTCManager.prototype._computeSAS; + + const baseline = await computeSAS.call(manager, keyMaterial, ' AA:BB ', 'CC:DD'); + const sameInputsNormalized = await computeSAS.call(manager, keyMaterial, 'aa:bb', ' cc:dd '); + const changedLocal = await computeSAS.call(manager, keyMaterial, 'AA:BC', 'CC:DD'); + const changedRemote = await computeSAS.call(manager, keyMaterial, 'AA:BB', 'CC:DE'); + + assert.equal(baseline, sameInputsNormalized); + assert.notEqual(baseline, changedLocal); + assert.notEqual(baseline, changedRemote); +} + +// SAS rejects non-string or empty fingerprints instead of allowing JS coercion. +{ + const manager = createSASManager(); + const keyMaterial = new Uint8Array([1, 2, 3, 4]); + const computeSAS = EnhancedSecureWebRTCManager.prototype._computeSAS; + const invalidFingerprints = [{ fingerprint: 'aa' }, ['aa'], null, '']; + + for (const invalidFingerprint of invalidFingerprints) { + await assert.rejects( + () => computeSAS.call(manager, keyMaterial, invalidFingerprint, 'CC:DD'), + /Security error: localFP must be a non-empty DTLS fingerprint string/ + ); + await assert.rejects( + () => computeSAS.call(manager, keyMaterial, 'AA:BB', invalidFingerprint), + /Security error: remoteFP must be a non-empty DTLS fingerprint string/ + ); + } +} + +// The salt is built only from normalized fingerprint strings. +{ + const manager = createSASManager(); + const keyMaterial = new Uint8Array([9, 8, 7, 6]); + let capturedSalt = ''; + const originalCryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'crypto'); + + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + subtle: { + importKey: (...args) => webcrypto.subtle.importKey(...args), + deriveBits: async (params, ...args) => { + capturedSalt = new TextDecoder().decode(params.salt); + return webcrypto.subtle.deriveBits(params, ...args); + } + } + } + }); + + try { + await EnhancedSecureWebRTCManager.prototype._computeSAS.call(manager, keyMaterial, ' AA:BB ', 'CC:DD '); + assert.equal(capturedSalt, 'webrtc-sas|aa:bb|cc:dd'); + assert.equal(capturedSalt.includes('[object Object]'), false); + } finally { + Object.defineProperty(globalThis, 'crypto', originalCryptoDescriptor); + } +} + +// Extraction returns a deterministic primary string for SAS binding. +{ + const manager = createSASManager(); + const sdp = [ + 'v=0', + 'a=fingerprint:sha-512 FF:EE', + 'a=fingerprint:sha-256 BB:BB', + 'a=fingerprint:sha-256 AA:AA' + ].join('\r\n'); + + assert.equal( + EnhancedSecureWebRTCManager.prototype._extractDTLSFingerprintFromSDP.call(manager, sdp), + 'AA:AA' + ); +} + console.log('SAS verification tests passed');