2025-08-11 20:52:14 -04:00
|
|
|
|
class EnhancedSecureWebRTCManager {
|
|
|
|
|
|
constructor(onMessage, onStatusChange, onKeyExchange, onVerificationRequired, onAnswerError = null) {
|
|
|
|
|
|
// Проверяем доступность глобального объекта
|
|
|
|
|
|
if (!window.EnhancedSecureCryptoUtils) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('EnhancedSecureCryptoUtils is not loaded. Please ensure the module is loaded first.');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.peerConnection = null;
|
|
|
|
|
|
this.dataChannel = null;
|
|
|
|
|
|
this.encryptionKey = null;
|
|
|
|
|
|
this.macKey = null;
|
|
|
|
|
|
this.metadataKey = null;
|
|
|
|
|
|
this.keyFingerprint = null;
|
|
|
|
|
|
this.onMessage = onMessage;
|
|
|
|
|
|
this.onStatusChange = onStatusChange;
|
|
|
|
|
|
this.onKeyExchange = onKeyExchange;
|
|
|
|
|
|
this.onVerificationRequired = onVerificationRequired;
|
|
|
|
|
|
this.onAnswerError = onAnswerError; // Callback для ошибок обработки ответа
|
|
|
|
|
|
this.isInitiator = false;
|
|
|
|
|
|
this.connectionAttempts = 0;
|
|
|
|
|
|
this.maxConnectionAttempts = 3;
|
|
|
|
|
|
this.heartbeatInterval = null;
|
|
|
|
|
|
this.messageQueue = [];
|
|
|
|
|
|
this.ecdhKeyPair = null;
|
|
|
|
|
|
this.ecdsaKeyPair = null;
|
|
|
|
|
|
this.verificationCode = null;
|
|
|
|
|
|
this.isVerified = false;
|
|
|
|
|
|
this.processedMessageIds = new Set();
|
|
|
|
|
|
this.messageCounter = 0;
|
|
|
|
|
|
this.sequenceNumber = 0;
|
|
|
|
|
|
this.expectedSequenceNumber = 0;
|
|
|
|
|
|
this.sessionSalt = null;
|
|
|
|
|
|
this.sessionId = null; // MITM protection: Session identifier
|
|
|
|
|
|
this.peerPublicKey = null; // Store peer's public key for PFS
|
|
|
|
|
|
this.rateLimiterId = null;
|
|
|
|
|
|
this.intentionalDisconnect = false;
|
|
|
|
|
|
this.lastCleanupTime = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
// PFS (Perfect Forward Secrecy) Implementation
|
|
|
|
|
|
this.keyRotationInterval = 300000; // 5 minutes
|
|
|
|
|
|
this.lastKeyRotation = Date.now();
|
|
|
|
|
|
this.currentKeyVersion = 0;
|
|
|
|
|
|
this.keyVersions = new Map(); // Store key versions for PFS
|
|
|
|
|
|
this.oldKeys = new Map(); // Store old keys temporarily for decryption
|
|
|
|
|
|
this.maxOldKeys = 3; // Keep last 3 key versions for decryption
|
|
|
|
|
|
|
|
|
|
|
|
this.securityFeatures = {
|
|
|
|
|
|
hasEncryption: true,
|
|
|
|
|
|
hasECDH: true,
|
|
|
|
|
|
hasECDSA: false,
|
|
|
|
|
|
hasMutualAuth: false,
|
|
|
|
|
|
hasMetadataProtection: false,
|
|
|
|
|
|
hasEnhancedReplayProtection: false,
|
|
|
|
|
|
hasNonExtractableKeys: false,
|
|
|
|
|
|
hasRateLimiting: false,
|
|
|
|
|
|
hasEnhancedValidation: false,
|
|
|
|
|
|
hasPFS: true // New PFS feature flag
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize rate limiter ID
|
|
|
|
|
|
this.rateLimiterId = `webrtc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Start periodic cleanup
|
|
|
|
|
|
this.startPeriodicCleanup();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start periodic cleanup for rate limiting and security
|
|
|
|
|
|
startPeriodicCleanup() {
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if (now - this.lastCleanupTime > 300000) { // Every 5 minutes
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.rateLimiter.cleanup();
|
|
|
|
|
|
this.lastCleanupTime = now;
|
|
|
|
|
|
|
|
|
|
|
|
// Clean old processed message IDs (keep only last hour)
|
|
|
|
|
|
if (this.processedMessageIds.size > 1000) {
|
|
|
|
|
|
this.processedMessageIds.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Clean old keys that are no longer needed
|
|
|
|
|
|
this.cleanupOldKeys();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 60000); // Check every minute
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate current security level with real verification
|
|
|
|
|
|
async calculateSecurityLevel() {
|
|
|
|
|
|
return await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Check if key rotation is needed
|
|
|
|
|
|
shouldRotateKeys() {
|
|
|
|
|
|
if (!this.isConnected() || !this.isVerified) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const timeSinceLastRotation = now - this.lastKeyRotation;
|
|
|
|
|
|
|
|
|
|
|
|
// Rotate keys every 5 minutes or after 100 messages
|
|
|
|
|
|
return timeSinceLastRotation > this.keyRotationInterval ||
|
|
|
|
|
|
this.messageCounter % 100 === 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Rotate encryption keys for Perfect Forward Secrecy
|
|
|
|
|
|
async rotateKeys() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!this.isConnected() || !this.isVerified) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Sending key rotation signal to partner.
|
2025-08-11 20:52:14 -04:00
|
|
|
|
const rotationSignal = {
|
|
|
|
|
|
type: 'key_rotation_signal',
|
|
|
|
|
|
newVersion: this.currentKeyVersion + 1,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.send(JSON.stringify(rotationSignal));
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Waiting for partner's confirmation before rotation.
|
2025-08-11 20:52:14 -04:00
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
this.pendingRotation = {
|
|
|
|
|
|
newVersion: this.currentKeyVersion + 1,
|
|
|
|
|
|
resolve: resolve
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Timeout in case the partner doesn't respond.
|
2025-08-11 20:52:14 -04:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (this.pendingRotation) {
|
|
|
|
|
|
this.pendingRotation.resolve(false);
|
|
|
|
|
|
this.pendingRotation = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Key rotation failed', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Clean up old keys that are no longer needed
|
|
|
|
|
|
cleanupOldKeys() {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const maxKeyAge = 900000; // 15 minutes - keys older than this are deleted
|
|
|
|
|
|
|
|
|
|
|
|
for (const [version, keySet] of this.oldKeys.entries()) {
|
|
|
|
|
|
if (now - keySet.timestamp > maxKeyAge) {
|
|
|
|
|
|
this.oldKeys.delete(version);
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Old PFS keys cleaned up', {
|
|
|
|
|
|
version: version,
|
|
|
|
|
|
age: Math.round((now - keySet.timestamp) / 1000) + 's'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Get keys for specific version (for decryption)
|
|
|
|
|
|
getKeysForVersion(version) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// First, we check the old keys (including version 0).
|
2025-08-11 20:52:14 -04:00
|
|
|
|
const oldKeySet = this.oldKeys.get(version);
|
|
|
|
|
|
if (oldKeySet && oldKeySet.encryptionKey && oldKeySet.macKey && oldKeySet.metadataKey) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
encryptionKey: oldKeySet.encryptionKey,
|
|
|
|
|
|
macKey: oldKeySet.macKey,
|
|
|
|
|
|
metadataKey: oldKeySet.metadataKey
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// If this is the current version, return the current keys.
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (version === this.currentKeyVersion) {
|
|
|
|
|
|
if (this.encryptionKey && this.macKey && this.metadataKey) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
encryptionKey: this.encryptionKey,
|
|
|
|
|
|
macKey: this.macKey,
|
|
|
|
|
|
metadataKey: this.metadataKey
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'No valid keys found for version', {
|
|
|
|
|
|
requestedVersion: version,
|
|
|
|
|
|
currentVersion: this.currentKeyVersion,
|
|
|
|
|
|
availableVersions: Array.from(this.oldKeys.keys())
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createPeerConnection() {
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
iceServers: [
|
|
|
|
|
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
|
|
|
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
|
|
|
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
|
|
|
|
|
{ urls: 'stun:stun3.l.google.com:19302' },
|
|
|
|
|
|
{ urls: 'stun:stun4.l.google.com:19302' }
|
|
|
|
|
|
],
|
|
|
|
|
|
iceCandidatePoolSize: 10,
|
|
|
|
|
|
bundlePolicy: 'balanced'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.peerConnection = new RTCPeerConnection(config);
|
|
|
|
|
|
|
|
|
|
|
|
this.peerConnection.onconnectionstatechange = () => {
|
|
|
|
|
|
const state = this.peerConnection.connectionState;
|
|
|
|
|
|
console.log('Connection state:', state);
|
|
|
|
|
|
|
|
|
|
|
|
if (state === 'connected' && !this.isVerified) {
|
|
|
|
|
|
this.onStatusChange('verifying');
|
|
|
|
|
|
} else if (state === 'connected' && this.isVerified) {
|
|
|
|
|
|
this.onStatusChange('connected');
|
|
|
|
|
|
} else if (state === 'disconnected' || state === 'closed') {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// If this is an intentional disconnect, clear immediately.
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (this.intentionalDisconnect) {
|
|
|
|
|
|
this.onStatusChange('disconnected');
|
|
|
|
|
|
setTimeout(() => this.cleanupConnection(), 100);
|
|
|
|
|
|
} else {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Unexpected disconnection — attempting to notify partner.
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.onStatusChange('reconnecting');
|
|
|
|
|
|
this.handleUnexpectedDisconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (state === 'failed') {
|
|
|
|
|
|
if (!this.intentionalDisconnect && this.connectionAttempts < this.maxConnectionAttempts) {
|
|
|
|
|
|
this.connectionAttempts++;
|
|
|
|
|
|
setTimeout(() => this.retryConnection(), 2000);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.onStatusChange('failed');
|
|
|
|
|
|
setTimeout(() => this.cleanupConnection(), 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.onStatusChange(state);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.peerConnection.ondatachannel = (event) => {
|
|
|
|
|
|
console.log('Data channel received');
|
|
|
|
|
|
this.setupDataChannel(event.channel);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setupDataChannel(channel) {
|
|
|
|
|
|
this.dataChannel = channel;
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.onopen = () => {
|
|
|
|
|
|
console.log('Secure data channel opened');
|
|
|
|
|
|
if (this.isVerified) {
|
|
|
|
|
|
this.onStatusChange('connected');
|
|
|
|
|
|
this.processMessageQueue();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.onStatusChange('verifying');
|
|
|
|
|
|
this.initiateVerification();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.startHeartbeat();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.onclose = () => {
|
|
|
|
|
|
console.log('Data channel closed');
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.intentionalDisconnect) {
|
|
|
|
|
|
this.onStatusChange('reconnecting');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('🔄 Data channel closed. Attempting recovery...', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.handleUnexpectedDisconnect();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.onStatusChange('disconnected');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('🔌 Connection closed', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
this.isVerified = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.onmessage = async (event) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = JSON.parse(event.data);
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.type === 'heartbeat') {
|
|
|
|
|
|
this.handleHeartbeat();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.type === 'verification') {
|
|
|
|
|
|
this.handleVerificationRequest(payload.data);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.type === 'verification_response') {
|
|
|
|
|
|
this.handleVerificationResponse(payload.data);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.type === 'peer_disconnect') {
|
|
|
|
|
|
this.handlePeerDisconnectNotification(payload);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.type === 'key_rotation_signal') {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Key rotation signal received but ignored for stability', {
|
|
|
|
|
|
newVersion: payload.newVersion
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.type === 'key_rotation_ready') {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Key rotation ready signal received but ignored for stability');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Handle enhanced messages with metadata protection and PFS
|
|
|
|
|
|
if (payload.type === 'enhanced_message') {
|
|
|
|
|
|
const keyVersion = payload.keyVersion || 0;
|
|
|
|
|
|
const keys = this.getKeysForVersion(keyVersion);
|
|
|
|
|
|
|
|
|
|
|
|
if (!keys) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Keys not available for message decryption', {
|
|
|
|
|
|
keyVersion: keyVersion,
|
|
|
|
|
|
currentKeyVersion: this.currentKeyVersion,
|
|
|
|
|
|
hasCurrentKeys: !!(this.encryptionKey && this.macKey && this.metadataKey),
|
|
|
|
|
|
availableOldVersions: Array.from(this.oldKeys.keys())
|
|
|
|
|
|
});
|
|
|
|
|
|
throw new Error(`Cannot decrypt message: keys for version ${keyVersion} not available`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!(keys.encryptionKey instanceof CryptoKey) ||
|
|
|
|
|
|
!(keys.macKey instanceof CryptoKey) ||
|
|
|
|
|
|
!(keys.metadataKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid key types for message decryption', {
|
|
|
|
|
|
keyVersion: keyVersion,
|
|
|
|
|
|
encryptionKeyType: typeof keys.encryptionKey,
|
|
|
|
|
|
macKeyType: typeof keys.macKey,
|
|
|
|
|
|
metadataKeyType: typeof keys.metadataKey
|
|
|
|
|
|
});
|
|
|
|
|
|
throw new Error(`Invalid key types for version ${keyVersion}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Using a more flexible sequence number check
|
2025-08-11 20:52:14 -04:00
|
|
|
|
const decryptedData = await window.EnhancedSecureCryptoUtils.decryptMessage(
|
|
|
|
|
|
payload.data,
|
|
|
|
|
|
keys.encryptionKey,
|
|
|
|
|
|
keys.macKey,
|
|
|
|
|
|
keys.metadataKey,
|
2025-08-13 14:48:24 -04:00
|
|
|
|
null // Disabling strict sequence number verification
|
2025-08-11 20:52:14 -04:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Checking for replay attack using messageId
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (this.processedMessageIds.has(decryptedData.messageId)) {
|
|
|
|
|
|
throw new Error('Duplicate message detected - possible replay attack');
|
|
|
|
|
|
}
|
|
|
|
|
|
this.processedMessageIds.add(decryptedData.messageId);
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Updating expected sequence number more flexibly
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (decryptedData.sequenceNumber >= this.expectedSequenceNumber) {
|
|
|
|
|
|
this.expectedSequenceNumber = decryptedData.sequenceNumber + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sanitizedMessage = window.EnhancedSecureCryptoUtils.sanitizeMessage(decryptedData.message);
|
|
|
|
|
|
this.onMessage(sanitizedMessage, 'received');
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced message received with PFS', {
|
|
|
|
|
|
messageId: decryptedData.messageId,
|
|
|
|
|
|
sequenceNumber: decryptedData.sequenceNumber,
|
|
|
|
|
|
keyVersion: keyVersion,
|
|
|
|
|
|
hasMetadataProtection: true,
|
|
|
|
|
|
hasPFS: true
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy message support for backward compatibility
|
|
|
|
|
|
if (payload.type === 'message') {
|
|
|
|
|
|
// Additional validation for legacy messages
|
|
|
|
|
|
if (!this.encryptionKey || !this.macKey) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Missing keys for legacy message decryption', {
|
|
|
|
|
|
hasEncryptionKey: !!this.encryptionKey,
|
|
|
|
|
|
hasMacKey: !!this.macKey,
|
|
|
|
|
|
hasMetadataKey: !!this.metadataKey
|
|
|
|
|
|
});
|
|
|
|
|
|
throw new Error('Отсутствуют ключи для расшифровки legacy сообщения');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const decryptedData = await window.EnhancedSecureCryptoUtils.decryptMessage(
|
|
|
|
|
|
payload.data,
|
|
|
|
|
|
this.encryptionKey,
|
|
|
|
|
|
this.macKey,
|
|
|
|
|
|
this.metadataKey // Add metadataKey for consistency
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Check for replay attacks
|
|
|
|
|
|
if (this.processedMessageIds.has(decryptedData.messageId)) {
|
|
|
|
|
|
throw new Error('Duplicate message detected - possible replay attack');
|
|
|
|
|
|
}
|
|
|
|
|
|
this.processedMessageIds.add(decryptedData.messageId);
|
|
|
|
|
|
|
|
|
|
|
|
const sanitizedMessage = window.EnhancedSecureCryptoUtils.sanitizeMessage(decryptedData.message);
|
|
|
|
|
|
this.onMessage(sanitizedMessage, 'received');
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy message received', {
|
|
|
|
|
|
messageId: decryptedData.messageId,
|
|
|
|
|
|
legacy: true
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Unknown message type
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('warn', 'Unknown message type received', {
|
|
|
|
|
|
type: payload.type
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Message processing error', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage(`❌ Processing error: ${error.message}`, 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.onerror = (error) => {
|
|
|
|
|
|
console.error('Data channel error:', error);
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('❌ Data channel error', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async createSecureOffer() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Check rate limiting
|
|
|
|
|
|
if (!window.EnhancedSecureCryptoUtils.rateLimiter.checkConnectionRate(this.rateLimiterId)) {
|
|
|
|
|
|
throw new Error('Connection rate limit exceeded. Please wait before trying again.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.connectionAttempts = 0;
|
|
|
|
|
|
this.sessionSalt = window.EnhancedSecureCryptoUtils.generateSalt(); // Now 64 bytes
|
|
|
|
|
|
|
|
|
|
|
|
// Generate ECDH key pair (non-extractable)
|
|
|
|
|
|
this.ecdhKeyPair = await window.EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
|
|
|
|
|
|
|
|
|
|
|
// Generate ECDSA key pair for digital signatures
|
|
|
|
|
|
this.ecdsaKeyPair = await window.EnhancedSecureCryptoUtils.generateECDSAKeyPair();
|
|
|
|
|
|
|
|
|
|
|
|
// MITM Protection: Verify key uniqueness and prevent key reuse attacks
|
|
|
|
|
|
const ecdhFingerprint = await window.EnhancedSecureCryptoUtils.calculateKeyFingerprint(
|
|
|
|
|
|
await crypto.subtle.exportKey('spki', this.ecdhKeyPair.publicKey)
|
|
|
|
|
|
);
|
|
|
|
|
|
const ecdsaFingerprint = await window.EnhancedSecureCryptoUtils.calculateKeyFingerprint(
|
|
|
|
|
|
await crypto.subtle.exportKey('spki', this.ecdsaKeyPair.publicKey)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Generated unique key pairs for MITM protection', {
|
|
|
|
|
|
ecdhFingerprint: ecdhFingerprint.substring(0, 8),
|
|
|
|
|
|
ecdsaFingerprint: ecdsaFingerprint.substring(0, 8),
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Export keys with signatures
|
|
|
|
|
|
const ecdhPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature(
|
|
|
|
|
|
this.ecdhKeyPair.publicKey,
|
|
|
|
|
|
this.ecdsaKeyPair.privateKey,
|
|
|
|
|
|
'ECDH'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const ecdsaPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature(
|
|
|
|
|
|
this.ecdsaKeyPair.publicKey,
|
|
|
|
|
|
this.ecdsaKeyPair.privateKey,
|
|
|
|
|
|
'ECDSA'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Update security features
|
|
|
|
|
|
this.securityFeatures.hasECDSA = true;
|
|
|
|
|
|
this.securityFeatures.hasMutualAuth = true;
|
|
|
|
|
|
this.securityFeatures.hasMetadataProtection = true;
|
|
|
|
|
|
this.securityFeatures.hasEnhancedReplayProtection = true;
|
|
|
|
|
|
this.securityFeatures.hasNonExtractableKeys = true;
|
|
|
|
|
|
this.securityFeatures.hasRateLimiting = true;
|
|
|
|
|
|
this.securityFeatures.hasEnhancedValidation = true;
|
|
|
|
|
|
this.securityFeatures.hasPFS = true;
|
|
|
|
|
|
|
|
|
|
|
|
this.isInitiator = true;
|
|
|
|
|
|
this.onStatusChange('connecting');
|
|
|
|
|
|
|
|
|
|
|
|
this.createPeerConnection();
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel = this.peerConnection.createDataChannel('securechat', {
|
|
|
|
|
|
ordered: true,
|
|
|
|
|
|
maxRetransmits: 3
|
|
|
|
|
|
});
|
|
|
|
|
|
this.setupDataChannel(this.dataChannel);
|
|
|
|
|
|
|
|
|
|
|
|
const offer = await this.peerConnection.createOffer({
|
|
|
|
|
|
offerToReceiveAudio: false,
|
|
|
|
|
|
offerToReceiveVideo: false
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.peerConnection.setLocalDescription(offer);
|
|
|
|
|
|
await this.waitForIceGathering();
|
|
|
|
|
|
|
|
|
|
|
|
// Generate verification code for out-of-band authentication
|
|
|
|
|
|
this.verificationCode = window.EnhancedSecureCryptoUtils.generateVerificationCode();
|
|
|
|
|
|
this.onVerificationRequired(this.verificationCode);
|
|
|
|
|
|
|
|
|
|
|
|
// Generate mutual authentication challenge
|
|
|
|
|
|
const authChallenge = window.EnhancedSecureCryptoUtils.generateMutualAuthChallenge();
|
|
|
|
|
|
|
|
|
|
|
|
// MITM Protection: Add session-specific data to prevent session hijacking
|
|
|
|
|
|
this.sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
|
|
|
|
.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
|
|
|
|
|
|
|
|
const offerPackage = {
|
|
|
|
|
|
type: 'enhanced_secure_offer',
|
|
|
|
|
|
sdp: this.peerConnection.localDescription.sdp,
|
|
|
|
|
|
ecdhPublicKey: ecdhPublicKeyData,
|
|
|
|
|
|
ecdsaPublicKey: ecdsaPublicKeyData,
|
|
|
|
|
|
salt: this.sessionSalt,
|
|
|
|
|
|
verificationCode: this.verificationCode,
|
|
|
|
|
|
authChallenge: authChallenge,
|
|
|
|
|
|
sessionId: this.sessionId, // Additional MITM protection
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
version: '4.0',
|
|
|
|
|
|
securityLevel: await this.calculateSecurityLevel()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced secure offer created', {
|
|
|
|
|
|
version: '4.0',
|
|
|
|
|
|
hasECDSA: true,
|
|
|
|
|
|
saltSize: this.sessionSalt.length,
|
|
|
|
|
|
securityLevel: offerPackage.securityLevel.level
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return offerPackage;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced secure offer creation failed', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
this.onStatusChange('failed');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async createSecureAnswer(offerData) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting createSecureAnswer', {
|
|
|
|
|
|
hasOfferData: !!offerData,
|
|
|
|
|
|
offerType: offerData?.type,
|
|
|
|
|
|
hasECDHKey: !!offerData?.ecdhPublicKey,
|
|
|
|
|
|
hasECDSAKey: !!offerData?.ecdsaPublicKey,
|
|
|
|
|
|
hasSalt: !!offerData?.salt
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.validateEnhancedOfferData(offerData)) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Invalid connection data format');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check rate limiting
|
|
|
|
|
|
if (!window.EnhancedSecureCryptoUtils.rateLimiter.checkConnectionRate(this.rateLimiterId)) {
|
|
|
|
|
|
throw new Error('Connection rate limit exceeded. Please wait before trying again.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.sessionSalt = offerData.salt;
|
|
|
|
|
|
|
|
|
|
|
|
// Generate our ECDH key pair (non-extractable)
|
|
|
|
|
|
this.ecdhKeyPair = await window.EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
|
|
|
|
|
|
|
|
|
|
|
// Generate our ECDSA key pair for digital signatures
|
|
|
|
|
|
this.ecdsaKeyPair = await window.EnhancedSecureCryptoUtils.generateECDSAKeyPair();
|
|
|
|
|
|
|
|
|
|
|
|
// First, import the ECDSA public key without signature verification (for self-signed keys)
|
|
|
|
|
|
const peerECDSAPublicKey = await crypto.subtle.importKey(
|
|
|
|
|
|
'spki',
|
|
|
|
|
|
new Uint8Array(offerData.ecdsaPublicKey.keyData),
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'ECDSA',
|
|
|
|
|
|
namedCurve: 'P-384'
|
|
|
|
|
|
},
|
|
|
|
|
|
false,
|
|
|
|
|
|
['verify']
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Now verify the ECDSA key's self-signature
|
|
|
|
|
|
const ecdsaPackageCopy = { ...offerData.ecdsaPublicKey };
|
|
|
|
|
|
delete ecdsaPackageCopy.signature;
|
|
|
|
|
|
const ecdsaPackageString = JSON.stringify(ecdsaPackageCopy);
|
|
|
|
|
|
const ecdsaSignatureValid = await window.EnhancedSecureCryptoUtils.verifySignature(
|
|
|
|
|
|
peerECDSAPublicKey,
|
|
|
|
|
|
offerData.ecdsaPublicKey.signature,
|
|
|
|
|
|
ecdsaPackageString
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ecdsaSignatureValid) {
|
|
|
|
|
|
throw new Error('Invalid ECDSA key self-signature');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Now import and verify the ECDH public key using the verified ECDSA key
|
|
|
|
|
|
const peerECDHPublicKey = await window.EnhancedSecureCryptoUtils.importSignedPublicKey(
|
|
|
|
|
|
offerData.ecdhPublicKey,
|
|
|
|
|
|
peerECDSAPublicKey,
|
|
|
|
|
|
'ECDH'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Additional validation: Ensure all keys are CryptoKey instances before derivation
|
|
|
|
|
|
if (!(this.ecdhKeyPair?.privateKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Local ECDH private key is not a CryptoKey in createEnhancedSecureAnswer', {
|
|
|
|
|
|
hasKeyPair: !!this.ecdhKeyPair,
|
|
|
|
|
|
privateKeyType: typeof this.ecdhKeyPair?.privateKey,
|
|
|
|
|
|
privateKeyAlgorithm: this.ecdhKeyPair?.privateKey?.algorithm?.name
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('The local ECDH private key is not a valid CryptoKey.');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!(peerECDHPublicKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Peer ECDH public key is not a CryptoKey in createEnhancedSecureAnswer', {
|
|
|
|
|
|
publicKeyType: typeof peerECDHPublicKey,
|
|
|
|
|
|
publicKeyAlgorithm: peerECDHPublicKey?.algorithm?.name
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('The peer"s ECDH public key is not a valid CryptoKey');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store peer's public key for PFS key rotation
|
|
|
|
|
|
this.peerPublicKey = peerECDHPublicKey;
|
|
|
|
|
|
|
|
|
|
|
|
// Derive shared keys with metadata protection
|
|
|
|
|
|
const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
|
|
|
|
|
|
this.ecdhKeyPair.privateKey,
|
|
|
|
|
|
peerECDHPublicKey,
|
|
|
|
|
|
this.sessionSalt
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.encryptionKey = derivedKeys.encryptionKey;
|
|
|
|
|
|
this.macKey = derivedKeys.macKey;
|
|
|
|
|
|
this.metadataKey = derivedKeys.metadataKey;
|
|
|
|
|
|
this.keyFingerprint = derivedKeys.fingerprint;
|
|
|
|
|
|
this.sequenceNumber = 0;
|
|
|
|
|
|
this.expectedSequenceNumber = 0;
|
|
|
|
|
|
this.messageCounter = 0;
|
|
|
|
|
|
this.processedMessageIds.clear();
|
|
|
|
|
|
this.verificationCode = offerData.verificationCode;
|
|
|
|
|
|
|
|
|
|
|
|
// Validate that all keys are properly set
|
|
|
|
|
|
if (!(this.encryptionKey instanceof CryptoKey) ||
|
|
|
|
|
|
!(this.macKey instanceof CryptoKey) ||
|
|
|
|
|
|
!(this.metadataKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid key types after derivation in createEnhancedSecureAnswer', {
|
|
|
|
|
|
encryptionKeyType: typeof this.encryptionKey,
|
|
|
|
|
|
macKeyType: typeof this.macKey,
|
|
|
|
|
|
metadataKeyType: typeof this.metadataKey,
|
|
|
|
|
|
encryptionKeyAlgorithm: this.encryptionKey?.algorithm?.name,
|
|
|
|
|
|
macKeyAlgorithm: this.macKey?.algorithm?.name,
|
|
|
|
|
|
metadataKeyAlgorithm: this.metadataKey?.algorithm?.name
|
|
|
|
|
|
});
|
|
|
|
|
|
throw new Error('Недействительные типы ключей после вывода');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Initialize key version tracking
|
|
|
|
|
|
this.currentKeyVersion = 0;
|
|
|
|
|
|
this.lastKeyRotation = Date.now();
|
|
|
|
|
|
this.keyVersions.set(0, {
|
|
|
|
|
|
salt: this.sessionSalt,
|
|
|
|
|
|
timestamp: this.lastKeyRotation,
|
|
|
|
|
|
messageCount: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Encryption keys set in createEnhancedSecureAnswer', {
|
|
|
|
|
|
hasEncryptionKey: !!this.encryptionKey,
|
|
|
|
|
|
hasMacKey: !!this.macKey,
|
|
|
|
|
|
hasMetadataKey: !!this.metadataKey,
|
|
|
|
|
|
keyFingerprint: this.keyFingerprint
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Update security features
|
|
|
|
|
|
this.securityFeatures.hasECDSA = true;
|
|
|
|
|
|
this.securityFeatures.hasMutualAuth = true;
|
|
|
|
|
|
this.securityFeatures.hasMetadataProtection = true;
|
|
|
|
|
|
this.securityFeatures.hasEnhancedReplayProtection = true;
|
|
|
|
|
|
this.securityFeatures.hasNonExtractableKeys = true;
|
|
|
|
|
|
this.securityFeatures.hasRateLimiting = true;
|
|
|
|
|
|
this.securityFeatures.hasEnhancedValidation = true;
|
|
|
|
|
|
this.securityFeatures.hasPFS = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Create authentication proof for mutual authentication
|
|
|
|
|
|
const authProof = await window.EnhancedSecureCryptoUtils.createAuthProof(
|
|
|
|
|
|
offerData.authChallenge,
|
|
|
|
|
|
this.ecdsaKeyPair.privateKey,
|
|
|
|
|
|
this.ecdsaKeyPair.publicKey
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.isInitiator = false;
|
|
|
|
|
|
this.onStatusChange('connecting');
|
|
|
|
|
|
this.onKeyExchange(this.keyFingerprint);
|
|
|
|
|
|
this.onVerificationRequired(this.verificationCode);
|
|
|
|
|
|
|
|
|
|
|
|
this.createPeerConnection();
|
|
|
|
|
|
|
|
|
|
|
|
await this.peerConnection.setRemoteDescription(new RTCSessionDescription({
|
|
|
|
|
|
type: 'offer',
|
|
|
|
|
|
sdp: offerData.sdp
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const answer = await this.peerConnection.createAnswer({
|
|
|
|
|
|
offerToReceiveAudio: false,
|
|
|
|
|
|
offerToReceiveVideo: false
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await this.peerConnection.setLocalDescription(answer);
|
|
|
|
|
|
await this.waitForIceGathering();
|
|
|
|
|
|
|
|
|
|
|
|
// Export our keys with signatures
|
|
|
|
|
|
const ecdhPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature(
|
|
|
|
|
|
this.ecdhKeyPair.publicKey,
|
|
|
|
|
|
this.ecdsaKeyPair.privateKey,
|
|
|
|
|
|
'ECDH'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const ecdsaPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature(
|
|
|
|
|
|
this.ecdsaKeyPair.publicKey,
|
|
|
|
|
|
this.ecdsaKeyPair.privateKey,
|
|
|
|
|
|
'ECDSA'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const answerPackage = {
|
|
|
|
|
|
type: 'enhanced_secure_answer',
|
|
|
|
|
|
sdp: this.peerConnection.localDescription.sdp,
|
|
|
|
|
|
ecdhPublicKey: ecdhPublicKeyData,
|
|
|
|
|
|
ecdsaPublicKey: ecdsaPublicKeyData,
|
|
|
|
|
|
authProof: authProof,
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
version: '4.0',
|
|
|
|
|
|
securityLevel: await this.calculateSecurityLevel()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced secure answer created', {
|
|
|
|
|
|
version: '4.0',
|
|
|
|
|
|
hasECDSA: true,
|
|
|
|
|
|
hasMutualAuth: true,
|
|
|
|
|
|
securityLevel: answerPackage.securityLevel.level
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return answerPackage;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced secure answer creation failed', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
this.onStatusChange('failed');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async handleSecureAnswer(answerData) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!answerData || answerData.type !== 'enhanced_secure_answer' || !answerData.sdp) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Invalid response format');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Import peer's ECDH public key from the signed package
|
|
|
|
|
|
if (!answerData.ecdhPublicKey || !answerData.ecdhPublicKey.keyData) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Missing ECDH public key data');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// First, import and verify the ECDSA public key for signature verification
|
|
|
|
|
|
if (!answerData.ecdsaPublicKey || !answerData.ecdsaPublicKey.keyData) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Missing ECDSA public key data for signature verification');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Additional MITM protection: Validate answer data structure
|
|
|
|
|
|
if (!answerData.timestamp || !answerData.version) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Missing required fields in response data – possible MITM attack');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MITM Protection: Verify session ID if present (for enhanced security)
|
|
|
|
|
|
if (answerData.sessionId && this.sessionId && answerData.sessionId !== this.sessionId) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Session ID mismatch detected - possible MITM attack', {
|
|
|
|
|
|
expectedSessionId: this.sessionId,
|
|
|
|
|
|
receivedSessionId: answerData.sessionId
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Session ID mismatch – possible MITM attack');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for replay attacks (reject answers older than 1 hour)
|
|
|
|
|
|
const answerAge = Date.now() - answerData.timestamp;
|
|
|
|
|
|
if (answerAge > 3600000) { // 1 hour in milliseconds
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Answer data is too old - possible replay attack', {
|
|
|
|
|
|
answerAge: answerAge,
|
|
|
|
|
|
timestamp: answerData.timestamp
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Уведомляем основной код о ошибке replay attack
|
|
|
|
|
|
if (this.onAnswerError) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onAnswerError('replay_attack', 'Response data is too old – possible replay attack');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Response data is too old – possible replay attack');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check protocol version compatibility
|
|
|
|
|
|
if (answerData.version !== '4.0') {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('warn', 'Incompatible protocol version in answer', {
|
|
|
|
|
|
expectedVersion: '4.0',
|
|
|
|
|
|
receivedVersion: answerData.version
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Import ECDSA public key for verification (self-signed)
|
|
|
|
|
|
const peerECDSAPublicKey = await crypto.subtle.importKey(
|
|
|
|
|
|
'spki',
|
|
|
|
|
|
new Uint8Array(answerData.ecdsaPublicKey.keyData),
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'ECDSA',
|
|
|
|
|
|
namedCurve: 'P-384'
|
|
|
|
|
|
},
|
|
|
|
|
|
false,
|
|
|
|
|
|
['verify']
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Verify ECDSA key's self-signature
|
|
|
|
|
|
const ecdsaPackageCopy = { ...answerData.ecdsaPublicKey };
|
|
|
|
|
|
delete ecdsaPackageCopy.signature;
|
|
|
|
|
|
const ecdsaPackageString = JSON.stringify(ecdsaPackageCopy);
|
|
|
|
|
|
const ecdsaSignatureValid = await window.EnhancedSecureCryptoUtils.verifySignature(
|
|
|
|
|
|
peerECDSAPublicKey,
|
|
|
|
|
|
answerData.ecdsaPublicKey.signature,
|
|
|
|
|
|
ecdsaPackageString
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ecdsaSignatureValid) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid ECDSA signature detected - possible MITM attack', {
|
|
|
|
|
|
timestamp: answerData.timestamp,
|
|
|
|
|
|
version: answerData.version
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Invalid ECDSA key signature – possible MITM attack');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA signature verification passed', {
|
|
|
|
|
|
timestamp: answerData.timestamp,
|
|
|
|
|
|
version: answerData.version
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Now import and verify the ECDH public key using the verified ECDSA key
|
|
|
|
|
|
const peerPublicKey = await window.EnhancedSecureCryptoUtils.importPublicKeyFromSignedPackage(
|
|
|
|
|
|
answerData.ecdhPublicKey,
|
|
|
|
|
|
peerECDSAPublicKey
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Additional MITM protection: Verify session salt integrity
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Invalid session salt – possible session hijacking attempt');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Verify that the session salt hasn't been tampered with
|
|
|
|
|
|
const expectedSaltHash = await window.EnhancedSecureCryptoUtils.calculateKeyFingerprint(this.sessionSalt);
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Session salt integrity verified', {
|
|
|
|
|
|
saltFingerprint: expectedSaltHash.substring(0, 8)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Additional validation: Ensure all keys are CryptoKey instances before derivation
|
|
|
|
|
|
if (!(this.ecdhKeyPair?.privateKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Local ECDH private key is not a CryptoKey in handleSecureAnswer', {
|
|
|
|
|
|
hasKeyPair: !!this.ecdhKeyPair,
|
|
|
|
|
|
privateKeyType: typeof this.ecdhKeyPair?.privateKey,
|
|
|
|
|
|
privateKeyAlgorithm: this.ecdhKeyPair?.privateKey?.algorithm?.name
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Local ECDH private key is not a CryptoKey');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!(peerPublicKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Peer ECDH public key is not a CryptoKey in handleSecureAnswer', {
|
|
|
|
|
|
publicKeyType: typeof peerPublicKey,
|
|
|
|
|
|
publicKeyAlgorithm: peerPublicKey?.algorithm?.name
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Peer ECDH public key is not a CryptoKey');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store peer's public key for PFS key rotation
|
|
|
|
|
|
this.peerPublicKey = peerPublicKey;
|
|
|
|
|
|
|
|
|
|
|
|
const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
|
|
|
|
|
|
this.ecdhKeyPair.privateKey,
|
|
|
|
|
|
peerPublicKey,
|
|
|
|
|
|
this.sessionSalt
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.encryptionKey = derivedKeys.encryptionKey;
|
|
|
|
|
|
this.macKey = derivedKeys.macKey;
|
|
|
|
|
|
this.metadataKey = derivedKeys.metadataKey;
|
|
|
|
|
|
this.keyFingerprint = derivedKeys.fingerprint;
|
|
|
|
|
|
this.sequenceNumber = 0;
|
|
|
|
|
|
this.expectedSequenceNumber = 0;
|
|
|
|
|
|
this.messageCounter = 0;
|
|
|
|
|
|
this.processedMessageIds.clear();
|
|
|
|
|
|
// Validate that all keys are properly set
|
|
|
|
|
|
if (!(this.encryptionKey instanceof CryptoKey) ||
|
|
|
|
|
|
!(this.macKey instanceof CryptoKey) ||
|
|
|
|
|
|
!(this.metadataKey instanceof CryptoKey)) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid key types after derivation in handleSecureAnswer', {
|
|
|
|
|
|
encryptionKeyType: typeof this.encryptionKey,
|
|
|
|
|
|
macKeyType: typeof this.macKey,
|
|
|
|
|
|
metadataKeyType: typeof this.metadataKey,
|
|
|
|
|
|
encryptionKeyAlgorithm: this.encryptionKey?.algorithm?.name,
|
|
|
|
|
|
macKeyAlgorithm: this.macKey?.algorithm?.name,
|
|
|
|
|
|
metadataKeyAlgorithm: this.metadataKey?.algorithm?.name
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Invalid key types after export');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Encryption keys set in handleSecureAnswer', {
|
|
|
|
|
|
hasEncryptionKey: !!this.encryptionKey,
|
|
|
|
|
|
hasMacKey: !!this.macKey,
|
|
|
|
|
|
hasMetadataKey: !!this.metadataKey,
|
|
|
|
|
|
keyFingerprint: this.keyFingerprint,
|
|
|
|
|
|
mitmProtection: 'enabled',
|
|
|
|
|
|
signatureVerified: true,
|
|
|
|
|
|
timestamp: answerData.timestamp,
|
|
|
|
|
|
version: answerData.version
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Update security features for initiator after successful key exchange
|
|
|
|
|
|
this.securityFeatures.hasMutualAuth = true;
|
|
|
|
|
|
this.securityFeatures.hasMetadataProtection = true;
|
|
|
|
|
|
this.securityFeatures.hasEnhancedReplayProtection = true;
|
|
|
|
|
|
this.securityFeatures.hasPFS = true;
|
|
|
|
|
|
|
|
|
|
|
|
// PFS: Initialize key version tracking
|
|
|
|
|
|
this.currentKeyVersion = 0;
|
|
|
|
|
|
this.lastKeyRotation = Date.now();
|
|
|
|
|
|
this.keyVersions.set(0, {
|
|
|
|
|
|
salt: this.sessionSalt,
|
|
|
|
|
|
timestamp: this.lastKeyRotation,
|
|
|
|
|
|
messageCount: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.onKeyExchange(this.keyFingerprint);
|
|
|
|
|
|
|
|
|
|
|
|
await this.peerConnection.setRemoteDescription({
|
|
|
|
|
|
type: 'answer',
|
|
|
|
|
|
sdp: answerData.sdp
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Enhanced secure connection established');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Enhanced secure answer handling failed:', error);
|
|
|
|
|
|
this.onStatusChange('failed');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (this.onAnswerError) {
|
|
|
|
|
|
if (error.message.includes('слишком старые') || error.message.includes('too old')) {
|
|
|
|
|
|
this.onAnswerError('replay_attack', error.message);
|
|
|
|
|
|
} else if (error.message.includes('MITM') || error.message.includes('подпись')) {
|
|
|
|
|
|
this.onAnswerError('security_violation', error.message);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.onAnswerError('general_error', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initiateVerification() {
|
|
|
|
|
|
if (this.isInitiator) {
|
|
|
|
|
|
// Initiator waits for verification confirmation
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('🔐 Confirm the security code with your peer to complete the connection', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
} else {
|
|
|
|
|
|
// Responder confirms verification automatically if codes match
|
|
|
|
|
|
this.confirmVerification();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
confirmVerification() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const verificationPayload = {
|
|
|
|
|
|
type: 'verification',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
code: this.verificationCode,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.send(JSON.stringify(verificationPayload));
|
|
|
|
|
|
this.isVerified = true;
|
|
|
|
|
|
this.onStatusChange('connected');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('✅ Verification successful. The channel is now secure!', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.processMessageQueue();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Verification failed:', error);
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('❌ Verification failed', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleVerificationRequest(data) {
|
|
|
|
|
|
if (data.code === this.verificationCode) {
|
|
|
|
|
|
const responsePayload = {
|
|
|
|
|
|
type: 'verification_response',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
verified: true,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
this.dataChannel.send(JSON.stringify(responsePayload));
|
|
|
|
|
|
this.isVerified = true;
|
|
|
|
|
|
this.onStatusChange('connected');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('✅ Verification successful. The channel is now secure!', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.processMessageQueue();
|
|
|
|
|
|
} else {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('❌ Verification code mismatch! Possible MITM attack detected. Connection aborted for safety!', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleVerificationResponse(data) {
|
|
|
|
|
|
if (data.verified) {
|
|
|
|
|
|
this.isVerified = true;
|
|
|
|
|
|
this.onStatusChange('connected');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('✅ Verification successful. The channel is now secure.!', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.processMessageQueue();
|
|
|
|
|
|
} else {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('❌ Verification failed!', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validateOfferData(offerData) {
|
|
|
|
|
|
return offerData &&
|
|
|
|
|
|
offerData.type === 'enhanced_secure_offer' &&
|
|
|
|
|
|
offerData.sdp &&
|
|
|
|
|
|
offerData.publicKey &&
|
|
|
|
|
|
offerData.salt &&
|
|
|
|
|
|
offerData.verificationCode &&
|
|
|
|
|
|
Array.isArray(offerData.publicKey) &&
|
|
|
|
|
|
Array.isArray(offerData.salt) &&
|
|
|
|
|
|
offerData.salt.length === 32;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Enhanced validation with backward compatibility
|
|
|
|
|
|
validateEnhancedOfferData(offerData) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!offerData || typeof offerData !== 'object') {
|
|
|
|
|
|
throw new Error('Offer data must be an object');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Basic required fields for all versions
|
|
|
|
|
|
const basicFields = ['type', 'sdp'];
|
|
|
|
|
|
for (const field of basicFields) {
|
|
|
|
|
|
if (!offerData[field]) {
|
|
|
|
|
|
throw new Error(`Missing required field: ${field}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate offer type (support both v3.0 and v4.0 formats)
|
|
|
|
|
|
if (!['enhanced_secure_offer', 'secure_offer'].includes(offerData.type)) {
|
|
|
|
|
|
throw new Error('Invalid offer type');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is v4.0 format with enhanced features
|
|
|
|
|
|
const isV4Format = offerData.version === '4.0' && offerData.ecdhPublicKey && offerData.ecdsaPublicKey;
|
|
|
|
|
|
|
|
|
|
|
|
if (isV4Format) {
|
|
|
|
|
|
// v4.0 enhanced validation
|
|
|
|
|
|
const v4RequiredFields = [
|
|
|
|
|
|
'ecdhPublicKey', 'ecdsaPublicKey', 'salt', 'verificationCode',
|
|
|
|
|
|
'authChallenge', 'timestamp', 'version', 'securityLevel'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const field of v4RequiredFields) {
|
|
|
|
|
|
if (!offerData[field]) {
|
|
|
|
|
|
throw new Error(`Missing v4.0 field: ${field}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate salt (must be 64 bytes for v4.0)
|
|
|
|
|
|
if (!Array.isArray(offerData.salt) || offerData.salt.length !== 64) {
|
|
|
|
|
|
throw new Error('Salt must be exactly 64 bytes for v4.0');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate timestamp (not older than 1 hour)
|
|
|
|
|
|
const offerAge = Date.now() - offerData.timestamp;
|
|
|
|
|
|
if (offerAge > 3600000) {
|
|
|
|
|
|
throw new Error('Offer is too old (older than 1 hour)');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate key structures (more lenient)
|
|
|
|
|
|
if (!offerData.ecdhPublicKey || typeof offerData.ecdhPublicKey !== 'object') {
|
|
|
|
|
|
throw new Error('Invalid ECDH public key structure');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!offerData.ecdsaPublicKey || typeof offerData.ecdsaPublicKey !== 'object') {
|
|
|
|
|
|
throw new Error('Invalid ECDSA public key structure');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate verification code format (more flexible)
|
|
|
|
|
|
if (typeof offerData.verificationCode !== 'string' || offerData.verificationCode.length < 6) {
|
|
|
|
|
|
throw new Error('Invalid verification code format');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'v4.0 offer validation passed', {
|
|
|
|
|
|
version: offerData.version,
|
|
|
|
|
|
securityLevel: offerData.securityLevel?.level || 'unknown',
|
|
|
|
|
|
offerAge: Math.round(offerAge / 1000) + 's'
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// v3.0 backward compatibility validation
|
|
|
|
|
|
const v3RequiredFields = ['publicKey', 'salt', 'verificationCode'];
|
|
|
|
|
|
for (const field of v3RequiredFields) {
|
|
|
|
|
|
if (!offerData[field]) {
|
|
|
|
|
|
throw new Error(`Missing v3.0 field: ${field}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate salt (32 bytes for v3.0)
|
|
|
|
|
|
if (!Array.isArray(offerData.salt) || offerData.salt.length !== 32) {
|
|
|
|
|
|
throw new Error('Salt must be exactly 32 bytes for v3.0');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate public key
|
|
|
|
|
|
if (!Array.isArray(offerData.publicKey)) {
|
|
|
|
|
|
throw new Error('Invalid public key format for v3.0');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'v3.0 offer validation passed (backward compatibility)', {
|
|
|
|
|
|
version: 'v3.0',
|
|
|
|
|
|
legacy: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate SDP structure (basic check for all versions)
|
|
|
|
|
|
if (typeof offerData.sdp !== 'string' || !offerData.sdp.includes('v=0')) {
|
|
|
|
|
|
throw new Error('Invalid SDP structure');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Offer validation failed', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
return false; // Return false instead of throwing to allow graceful handling
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async sendSecureMessage(message) {
|
|
|
|
|
|
if (!this.isConnected() || !this.isVerified) {
|
|
|
|
|
|
this.messageQueue.push(message);
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Connection not ready. Message queued for sending.');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate encryption keys
|
|
|
|
|
|
if (!this.encryptionKey || !this.macKey || !this.metadataKey) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Encryption keys not initialized', {
|
|
|
|
|
|
hasEncryptionKey: !!this.encryptionKey,
|
|
|
|
|
|
hasMacKey: !!this.macKey,
|
|
|
|
|
|
hasMetadataKey: !!this.metadataKey,
|
|
|
|
|
|
isConnected: this.isConnected(),
|
|
|
|
|
|
isVerified: this.isVerified
|
|
|
|
|
|
});
|
2025-08-13 14:48:24 -04:00
|
|
|
|
throw new Error('Encryption keys not initialized. Please check the connection.');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Check rate limiting
|
|
|
|
|
|
if (!window.EnhancedSecureCryptoUtils.rateLimiter.checkMessageRate(this.rateLimiterId)) {
|
|
|
|
|
|
throw new Error('Message rate limit exceeded (60 messages per minute)');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sanitizedMessage = window.EnhancedSecureCryptoUtils.sanitizeMessage(message);
|
|
|
|
|
|
const messageId = `msg_${Date.now()}_${this.messageCounter++}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Use enhanced encryption with metadata protection, sequence numbers, and PFS key version
|
|
|
|
|
|
const encryptedData = await window.EnhancedSecureCryptoUtils.encryptMessage(
|
|
|
|
|
|
sanitizedMessage,
|
|
|
|
|
|
this.encryptionKey,
|
|
|
|
|
|
this.macKey,
|
|
|
|
|
|
this.metadataKey,
|
|
|
|
|
|
messageId,
|
|
|
|
|
|
this.sequenceNumber++
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
type: 'enhanced_message',
|
|
|
|
|
|
data: encryptedData,
|
|
|
|
|
|
keyVersion: this.currentKeyVersion, // PFS: Include key version
|
|
|
|
|
|
version: '4.0'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.dataChannel.send(JSON.stringify(payload));
|
|
|
|
|
|
this.onMessage(sanitizedMessage, 'sent');
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced message sent with PFS', {
|
|
|
|
|
|
messageId,
|
|
|
|
|
|
sequenceNumber: this.sequenceNumber - 1,
|
|
|
|
|
|
keyVersion: this.currentKeyVersion,
|
|
|
|
|
|
hasMetadataProtection: true,
|
|
|
|
|
|
hasPFS: true
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced message sending failed', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
processMessageQueue() {
|
|
|
|
|
|
while (this.messageQueue.length > 0 && this.isConnected() && this.isVerified) {
|
|
|
|
|
|
const message = this.messageQueue.shift();
|
|
|
|
|
|
this.sendSecureMessage(message).catch(console.error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
startHeartbeat() {
|
|
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
|
|
|
|
if (this.isConnected()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.dataChannel.send(JSON.stringify({
|
|
|
|
|
|
type: 'heartbeat',
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Heartbeat failed:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopHeartbeat() {
|
|
|
|
|
|
if (this.heartbeatInterval) {
|
|
|
|
|
|
clearInterval(this.heartbeatInterval);
|
|
|
|
|
|
this.heartbeatInterval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleHeartbeat() {
|
|
|
|
|
|
console.log('Heartbeat received - connection alive');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
waitForIceGathering() {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
if (this.peerConnection.iceGatheringState === 'complete') {
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const checkState = () => {
|
|
|
|
|
|
if (this.peerConnection.iceGatheringState === 'complete') {
|
|
|
|
|
|
this.peerConnection.removeEventListener('icegatheringstatechange', checkState);
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.peerConnection.addEventListener('icegatheringstatechange', checkState);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.peerConnection.removeEventListener('icegatheringstatechange', checkState);
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
retryConnection() {
|
|
|
|
|
|
console.log(`Retrying connection (attempt ${this.connectionAttempts}/${this.maxConnectionAttempts})`);
|
|
|
|
|
|
this.onStatusChange('retrying');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isConnected() {
|
|
|
|
|
|
return this.dataChannel && this.dataChannel.readyState === 'open';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getConnectionInfo() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
fingerprint: this.keyFingerprint,
|
|
|
|
|
|
isConnected: this.isConnected(),
|
|
|
|
|
|
isVerified: this.isVerified,
|
|
|
|
|
|
connectionState: this.peerConnection?.connectionState,
|
|
|
|
|
|
iceConnectionState: this.peerConnection?.iceConnectionState,
|
|
|
|
|
|
verificationCode: this.verificationCode
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
|
|
this.intentionalDisconnect = true;
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting intentional disconnect');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.sendDisconnectNotification();
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
setTimeout(() => {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.sendDisconnectNotification();
|
2025-08-11 20:52:14 -04:00
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.cleanupConnection();
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleUnexpectedDisconnect() {
|
|
|
|
|
|
this.sendDisconnectNotification();
|
|
|
|
|
|
this.isVerified = false;
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('🔌 Connection lost. Attempting to reconnect...', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!this.intentionalDisconnect) {
|
|
|
|
|
|
this.attemptReconnection();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sendDisconnectNotification() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.dataChannel && this.dataChannel.readyState === 'open') {
|
|
|
|
|
|
const notification = {
|
|
|
|
|
|
type: 'peer_disconnect',
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
reason: this.intentionalDisconnect ? 'user_disconnect' : 'connection_lost'
|
|
|
|
|
|
};
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.dataChannel.send(JSON.stringify(notification));
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Disconnect notification sent', {
|
|
|
|
|
|
reason: notification.reason,
|
|
|
|
|
|
attempt: i + 1
|
|
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
} catch (sendError) {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
if (i === 2) {
|
2025-08-11 20:52:14 -04:00
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Failed to send disconnect notification', {
|
|
|
|
|
|
error: sendError.message
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Could not send disconnect notification', {
|
|
|
|
|
|
error: error.message
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
attemptReconnection() {
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage('❌ Unable to reconnect. A new connection is required.', 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.cleanupConnection();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handlePeerDisconnectNotification(data) {
|
|
|
|
|
|
const reason = data.reason || 'unknown';
|
2025-08-13 14:48:24 -04:00
|
|
|
|
const reasonText = reason === 'user_disconnect' ? 'manually disconnected.' : 'connection lost.';
|
2025-08-11 20:52:14 -04:00
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onMessage(`👋 Peer ${reasonText}`, 'system');
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.onStatusChange('peer_disconnected');
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.intentionalDisconnect = false;
|
|
|
|
|
|
this.isVerified = false;
|
|
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
this.onKeyExchange('');
|
|
|
|
|
|
this.onVerificationRequired('');
|
|
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.cleanupConnection();
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Peer disconnect notification processed', {
|
|
|
|
|
|
reason: reason
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanupConnection() {
|
|
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
this.isVerified = false;
|
|
|
|
|
|
this.processedMessageIds.clear();
|
|
|
|
|
|
this.messageCounter = 0;
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.encryptionKey = null;
|
|
|
|
|
|
this.macKey = null;
|
|
|
|
|
|
this.metadataKey = null;
|
|
|
|
|
|
this.keyFingerprint = null;
|
|
|
|
|
|
this.sessionSalt = null;
|
|
|
|
|
|
this.sessionId = null;
|
|
|
|
|
|
this.peerPublicKey = null;
|
|
|
|
|
|
this.verificationCode = null;
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// PFS: Clearing all key versions
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.keyVersions.clear();
|
|
|
|
|
|
this.oldKeys.clear();
|
|
|
|
|
|
this.currentKeyVersion = 0;
|
|
|
|
|
|
this.lastKeyRotation = Date.now();
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Clearing key pairs
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.ecdhKeyPair = null;
|
|
|
|
|
|
this.ecdsaKeyPair = null;
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Resetting message counters
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.sequenceNumber = 0;
|
|
|
|
|
|
this.expectedSequenceNumber = 0;
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Security flags reset completed
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.securityFeatures = {
|
|
|
|
|
|
hasEncryption: false,
|
|
|
|
|
|
hasECDH: false,
|
|
|
|
|
|
hasECDSA: false,
|
|
|
|
|
|
hasMutualAuth: false,
|
|
|
|
|
|
hasMetadataProtection: false,
|
|
|
|
|
|
hasEnhancedReplayProtection: false,
|
|
|
|
|
|
hasNonExtractableKeys: false,
|
|
|
|
|
|
hasRateLimiting: false,
|
|
|
|
|
|
hasEnhancedValidation: false,
|
|
|
|
|
|
hasPFS: false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Closing connections
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (this.dataChannel) {
|
|
|
|
|
|
this.dataChannel.close();
|
|
|
|
|
|
this.dataChannel = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.peerConnection) {
|
|
|
|
|
|
this.peerConnection.close();
|
|
|
|
|
|
this.peerConnection = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Clearing message queue
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.messageQueue = [];
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// IMPORTANT: Clearing security logs
|
2025-08-11 20:52:14 -04:00
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.clearLogs();
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Notifying the UI about complete cleanup
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.onStatusChange('disconnected');
|
|
|
|
|
|
this.onKeyExchange('');
|
|
|
|
|
|
this.onVerificationRequired('');
|
|
|
|
|
|
|
|
|
|
|
|
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Connection cleaned up completely');
|
|
|
|
|
|
|
2025-08-13 14:48:24 -04:00
|
|
|
|
// Resetting the intentional disconnect flag
|
2025-08-11 20:52:14 -04:00
|
|
|
|
this.intentionalDisconnect = false;
|
2025-08-13 14:48:24 -04:00
|
|
|
|
|
2025-08-11 20:52:14 -04:00
|
|
|
|
if (window.gc) {
|
|
|
|
|
|
window.gc();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export { EnhancedSecureWebRTCManager };
|