fix: prevent encryption key loss and IndexedDB connection errors
- Disable timer-based key rotation for Double Ratchet mode - Auto-reinitialize encryption keys when missing but ECDH available - Preserve active keys during periodic cleanup in ratchet sessions - Fix IndexedDB "database closing" errors with connection checking - Add individual transactions per queue item to prevent race conditions
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# SecureBit.chat v4.4.99
|
# SecureBit.chat v4.5.22
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -31,22 +31,16 @@ SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your p
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ What's New in v4.4.99
|
## ✨ What's New in v4.5.22
|
||||||
|
|
||||||
### 🔔 Secure Browser Notifications
|
### fix: prevent encryption key loss and IndexedDB connection errors
|
||||||
- Smart delivery when user is away from chat tab
|
|
||||||
- Cross-browser compatibility (Chrome, Firefox, Safari, Edge)
|
- Disable timer-based key rotation for Double Ratchet mode
|
||||||
- Page Visibility API integration with proper tab focus detection
|
- Auto-reinitialize encryption keys when missing but ECDH available
|
||||||
- XSS protection with text sanitization and URL validation
|
- Preserve active keys during periodic cleanup in ratchet sessions
|
||||||
- Rate limiting and spam protection
|
- Fix IndexedDB "database closing" errors with connection checking
|
||||||
- Automatic cleanup and memory management
|
- Add individual transactions per queue item to prevent race conditions
|
||||||
|
|
||||||
### 🧹 Code Cleanup & Architecture
|
|
||||||
- Removed session management logic for simplified architecture
|
|
||||||
- Eliminated experimental Bluetooth module
|
|
||||||
- Cleaned debug logging from production code
|
|
||||||
- Removed test functions from production build
|
|
||||||
- Enhanced error handling for production stability
|
|
||||||
|
|
||||||
### 🛡️ Security Enhancements
|
### 🛡️ Security Enhancements
|
||||||
- **ECDH + DTLS + SAS System** - Triple-layer security verification
|
- **ECDH + DTLS + SAS System** - Triple-layer security verification
|
||||||
@@ -170,7 +164,7 @@ Modern browser with WebRTC support (Chrome 60+, Firefox 60+, Safari 12+), HTTPS
|
|||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
**Current: v4.4.99** - Browser Notifications & Code Cleanup ✅
|
**Current: v4.5.22** - Browser Notifications & Code Cleanup ✅
|
||||||
|
|
||||||
**Next Releases:**
|
**Next Releases:**
|
||||||
|
|
||||||
@@ -336,7 +330,7 @@ MIT License - see **LICENSE** file for details.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Latest Release: v4.4.99** - Browser Notifications & Code Cleanup
|
**Latest Release: v4.5.22** - Browser Notifications & Code Cleanup
|
||||||
|
|
||||||
[🚀 Try Now](https://securebitchat.github.io/securebit-chat/) • [⭐ Star on GitHub](https://github.com/SecureBitChat/securebit-chat)
|
[🚀 Try Now](https://securebitchat.github.io/securebit-chat/) • [⭐ Star on GitHub](https://github.com/SecureBitChat/securebit-chat)
|
||||||
|
|
||||||
|
|||||||
Vendored
+52
-6
@@ -4724,7 +4724,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
};
|
};
|
||||||
this.onFileReceived = null;
|
this.onFileReceived = null;
|
||||||
this.onFileError = null;
|
this.onFileError = null;
|
||||||
this.keyRotationInterval = _EnhancedSecureWebRTCManager.TIMEOUTS.KEY_ROTATION_INTERVAL;
|
this.keyRotationInterval = null;
|
||||||
this.lastKeyRotation = Date.now();
|
this.lastKeyRotation = Date.now();
|
||||||
this.currentKeyVersion = 0;
|
this.currentKeyVersion = 0;
|
||||||
this.keyVersions = /* @__PURE__ */ new Map();
|
this.keyVersions = /* @__PURE__ */ new Map();
|
||||||
@@ -4761,6 +4761,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
packetPadding: this._config.packetPadding.enabled,
|
packetPadding: this._config.packetPadding.enabled,
|
||||||
antiFingerprinting: this._config.antiFingerprinting.enabled
|
antiFingerprinting: this._config.antiFingerprinting.enabled
|
||||||
});
|
});
|
||||||
|
this.sessionMode = "ratchet";
|
||||||
this._hardenDebugModeReferences();
|
this._hardenDebugModeReferences();
|
||||||
this._initializeUnifiedScheduler();
|
this._initializeUnifiedScheduler();
|
||||||
this._syncSecurityFeaturesWithTariff();
|
this._syncSecurityFeaturesWithTariff();
|
||||||
@@ -5357,9 +5358,6 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
if (this._keyStorageStats.activeKeys > 10) {
|
if (this._keyStorageStats.activeKeys > 10) {
|
||||||
this._secureLog("warn", "\u26A0\uFE0F High number of active keys detected. Consider rotation.");
|
this._secureLog("warn", "\u26A0\uFE0F High number of active keys detected. Consider rotation.");
|
||||||
}
|
}
|
||||||
if (Date.now() - (this._keyStorageStats.lastRotation || 0) > 36e5) {
|
|
||||||
this._rotateKeys();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Send heartbeat message (called by unified scheduler)
|
* Send heartbeat message (called by unified scheduler)
|
||||||
@@ -6791,7 +6789,12 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
async _performPeriodicMemoryCleanup() {
|
async _performPeriodicMemoryCleanup() {
|
||||||
try {
|
try {
|
||||||
this._secureMemoryManager.isCleaning = true;
|
this._secureMemoryManager.isCleaning = true;
|
||||||
|
const shouldPreserveActiveKeys = this.sessionMode === "ratchet" && this.isConnected && this.dataChannel && this.dataChannel.readyState === "open";
|
||||||
|
if (shouldPreserveActiveKeys) {
|
||||||
|
this._secureLog("debug", "\u{1F9F9} Skipping crypto key wipe during periodic cleanup (ratchet mode, active connection)");
|
||||||
|
} else {
|
||||||
this._secureCleanupCryptographicMaterials();
|
this._secureCleanupCryptographicMaterials();
|
||||||
|
}
|
||||||
if (this.messageQueue && this.messageQueue.length > 100) {
|
if (this.messageQueue && this.messageQueue.length > 100) {
|
||||||
const excessMessages = this.messageQueue.splice(0, this.messageQueue.length - 50);
|
const excessMessages = this.messageQueue.splice(0, this.messageQueue.length - 50);
|
||||||
excessMessages.forEach((message, index) => {
|
excessMessages.forEach((message, index) => {
|
||||||
@@ -7296,6 +7299,14 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
}
|
}
|
||||||
const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, "");
|
const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, "");
|
||||||
const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, "");
|
const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, "");
|
||||||
|
if (this.sessionMode === "ratchet" && normalizedExpected === normalizedReceived) {
|
||||||
|
this._secureLog("info", "Same fingerprint detected \u2014 skip MITM warning (ratchet mode)", {
|
||||||
|
context,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
this.isVerified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (normalizedReceived !== normalizedExpected) {
|
if (normalizedReceived !== normalizedExpected) {
|
||||||
this._secureLog("error", "DTLS fingerprint mismatch - possible MITM attack", {
|
this._secureLog("error", "DTLS fingerprint mismatch - possible MITM attack", {
|
||||||
context,
|
context,
|
||||||
@@ -7664,6 +7675,38 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
}
|
}
|
||||||
return hasAllKeys;
|
return hasAllKeys;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Attempt to reinitialize encryption keys if missing
|
||||||
|
* Uses existing ECDH key pair, peer public key, and session salt
|
||||||
|
* Returns true if keys were (re)initialized successfully
|
||||||
|
*/
|
||||||
|
async _tryReinitializeEncryptionKeys() {
|
||||||
|
try {
|
||||||
|
if (this.encryptionKey && this.macKey && this.metadataKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasECDH = !!(this.ecdhKeyPair?.privateKey && (this.peerPublicKey || this.peerECDHPublicKey));
|
||||||
|
const peerPublicKey = this.peerPublicKey || this.peerECDHPublicKey;
|
||||||
|
if (!hasECDH || !peerPublicKey || !this.sessionSalt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
|
||||||
|
this.ecdhKeyPair.privateKey,
|
||||||
|
peerPublicKey,
|
||||||
|
this.sessionSalt
|
||||||
|
);
|
||||||
|
await this._setEncryptionKeys(
|
||||||
|
derivedKeys.messageKey,
|
||||||
|
derivedKeys.macKey,
|
||||||
|
derivedKeys.metadataKey,
|
||||||
|
derivedKeys.fingerprint
|
||||||
|
);
|
||||||
|
return !!(this.encryptionKey && this.macKey && this.metadataKey);
|
||||||
|
} catch (error) {
|
||||||
|
this._secureLog("error", "Failed to reinitialize encryption keys", { error: error.message });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Checks whether a message is a file-transfer message
|
* Checks whether a message is a file-transfer message
|
||||||
* @param {string|object} data - message payload
|
* @param {string|object} data - message payload
|
||||||
@@ -9261,6 +9304,9 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
|||||||
throw new Error("Data channel not ready");
|
throw new Error("Data channel not ready");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (!(this.encryptionKey && this.macKey && this.metadataKey)) {
|
||||||
|
await this._tryReinitializeEncryptionKeys();
|
||||||
|
}
|
||||||
this._secureLog("debug", "sendMessage called", {
|
this._secureLog("debug", "sendMessage called", {
|
||||||
hasDataChannel: !!this.dataChannel,
|
hasDataChannel: !!this.dataChannel,
|
||||||
dataChannelReady: this.dataChannel?.readyState === "open",
|
dataChannelReady: this.dataChannel?.readyState === "open",
|
||||||
@@ -15265,7 +15311,7 @@ Right-click or Ctrl+click to disconnect`,
|
|||||||
React.createElement("p", {
|
React.createElement("p", {
|
||||||
key: "subtitle",
|
key: "subtitle",
|
||||||
className: "text-xs sm:text-sm text-muted hidden sm:block"
|
className: "text-xs sm:text-sm text-muted hidden sm:block"
|
||||||
}, "End-to-end freedom v4.4.99")
|
}, "End-to-end freedom v4.5.22")
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
// Status and Controls - Responsive
|
// Status and Controls - Responsive
|
||||||
@@ -16020,7 +16066,7 @@ function Roadmap() {
|
|||||||
},
|
},
|
||||||
// current and future phases
|
// current and future phases
|
||||||
{
|
{
|
||||||
version: "v4.4.99",
|
version: "v4.5.22",
|
||||||
title: "Enhanced Security Edition",
|
title: "Enhanced Security Edition",
|
||||||
status: "current",
|
status: "current",
|
||||||
date: "Now",
|
date: "Now",
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1688,7 +1688,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.4.99 - 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.5.22 - 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 (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "SecureBit.chat v4.4.99 - ECDH + DTLS + SAS",
|
"name": "SecureBit.chat v4.5.22 - 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
-1
@@ -1924,7 +1924,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.4.99 - 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.5.22 - 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) {
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ const EnhancedMinimalHeader = ({
|
|||||||
React.createElement('p', {
|
React.createElement('p', {
|
||||||
key: 'subtitle',
|
key: 'subtitle',
|
||||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||||
}, 'End-to-end freedom v4.4.99')
|
}, 'End-to-end freedom v4.5.22')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function Roadmap() {
|
|||||||
|
|
||||||
// current and future phases
|
// current and future phases
|
||||||
{
|
{
|
||||||
version: "v4.4.99",
|
version: "v4.5.22",
|
||||||
title: "Enhanced Security Edition",
|
title: "Enhanced Security Edition",
|
||||||
status: "current",
|
status: "current",
|
||||||
date: "Now",
|
date: "Now",
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
this.onFileError = null;
|
this.onFileError = null;
|
||||||
|
|
||||||
// PFS (Perfect Forward Secrecy) Implementation
|
// PFS (Perfect Forward Secrecy) Implementation
|
||||||
this.keyRotationInterval = EnhancedSecureWebRTCManager.TIMEOUTS.KEY_ROTATION_INTERVAL;
|
this.keyRotationInterval = null; // отключаем таймерную ротацию
|
||||||
this.lastKeyRotation = Date.now();
|
this.lastKeyRotation = Date.now();
|
||||||
this.currentKeyVersion = 0;
|
this.currentKeyVersion = 0;
|
||||||
this.keyVersions = new Map(); // Store key versions for PFS
|
this.keyVersions = new Map(); // Store key versions for PFS
|
||||||
@@ -372,6 +372,9 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
antiFingerprinting: this._config.antiFingerprinting.enabled
|
antiFingerprinting: this._config.antiFingerprinting.enabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Session mode: 'time' | 'ratchet' (Double Ratchet controls key lifecycle)
|
||||||
|
this.sessionMode = 'ratchet';
|
||||||
|
|
||||||
// XSS Hardening - replace all window.DEBUG_MODE references
|
// XSS Hardening - replace all window.DEBUG_MODE references
|
||||||
this._hardenDebugModeReferences();
|
this._hardenDebugModeReferences();
|
||||||
|
|
||||||
@@ -1183,9 +1186,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
this._secureLog('warn', '⚠️ High number of active keys detected. Consider rotation.');
|
this._secureLog('warn', '⚠️ High number of active keys detected. Consider rotation.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() - (this._keyStorageStats.lastRotation || 0) > 3600000) {
|
|
||||||
this._rotateKeys();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2900,8 +2900,13 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
try {
|
try {
|
||||||
this._secureMemoryManager.isCleaning = true;
|
this._secureMemoryManager.isCleaning = true;
|
||||||
|
|
||||||
// Clean up any remaining sensitive data
|
// Clean up sensitive data, but DO NOT wipe active crypto in ratchet session
|
||||||
|
const shouldPreserveActiveKeys = (this.sessionMode === 'ratchet') && this.isConnected && this.dataChannel && this.dataChannel.readyState === 'open';
|
||||||
|
if (shouldPreserveActiveKeys) {
|
||||||
|
this._secureLog('debug', '🧹 Skipping crypto key wipe during periodic cleanup (ratchet mode, active connection)');
|
||||||
|
} else {
|
||||||
this._secureCleanupCryptographicMaterials();
|
this._secureCleanupCryptographicMaterials();
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up message queue if it's too large
|
// Clean up message queue if it's too large
|
||||||
if (this.messageQueue && this.messageQueue.length > 100) {
|
if (this.messageQueue && this.messageQueue.length > 100) {
|
||||||
@@ -3545,6 +3550,16 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, '');
|
const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, '');
|
||||||
const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, '');
|
const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, '');
|
||||||
|
|
||||||
|
// Ratchet mode: if fingerprint hasn't changed, treat as verified and skip warnings
|
||||||
|
if (this.sessionMode === 'ratchet' && normalizedExpected === normalizedReceived) {
|
||||||
|
this._secureLog('info', 'Same fingerprint detected — skip MITM warning (ratchet mode)', {
|
||||||
|
context: context,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
this.isVerified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (normalizedReceived !== normalizedExpected) {
|
if (normalizedReceived !== normalizedExpected) {
|
||||||
this._secureLog('error', 'DTLS fingerprint mismatch - possible MITM attack', {
|
this._secureLog('error', 'DTLS fingerprint mismatch - possible MITM attack', {
|
||||||
context: context,
|
context: context,
|
||||||
@@ -4014,6 +4029,46 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
return hasAllKeys;
|
return hasAllKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to reinitialize encryption keys if missing
|
||||||
|
* Uses existing ECDH key pair, peer public key, and session salt
|
||||||
|
* Returns true if keys were (re)initialized successfully
|
||||||
|
*/
|
||||||
|
async _tryReinitializeEncryptionKeys() {
|
||||||
|
try {
|
||||||
|
// If keys already present, nothing to do
|
||||||
|
if (this.encryptionKey && this.macKey && this.metadataKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require ECDH materials and session salt to derive keys
|
||||||
|
const hasECDH = !!(this.ecdhKeyPair?.privateKey && (this.peerPublicKey || this.peerECDHPublicKey));
|
||||||
|
const peerPublicKey = this.peerPublicKey || this.peerECDHPublicKey;
|
||||||
|
if (!hasECDH || !peerPublicKey || !this.sessionSalt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive fresh keys
|
||||||
|
const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
|
||||||
|
this.ecdhKeyPair.privateKey,
|
||||||
|
peerPublicKey,
|
||||||
|
this.sessionSalt
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._setEncryptionKeys(
|
||||||
|
derivedKeys.messageKey,
|
||||||
|
derivedKeys.macKey,
|
||||||
|
derivedKeys.metadataKey,
|
||||||
|
derivedKeys.fingerprint
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!(this.encryptionKey && this.macKey && this.metadataKey);
|
||||||
|
} catch (error) {
|
||||||
|
this._secureLog('error', 'Failed to reinitialize encryption keys', { error: error.message });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a message is a file-transfer message
|
* Checks whether a message is a file-transfer message
|
||||||
* @param {string|object} data - message payload
|
* @param {string|object} data - message payload
|
||||||
@@ -6056,6 +6111,11 @@ async processOrderedPackets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ensure encryption keys are available; try to reinitialize if needed
|
||||||
|
if (!(this.encryptionKey && this.macKey && this.metadataKey)) {
|
||||||
|
await this._tryReinitializeEncryptionKeys();
|
||||||
|
}
|
||||||
|
|
||||||
this._secureLog('debug', 'sendMessage called', {
|
this._secureLog('debug', 'sendMessage called', {
|
||||||
hasDataChannel: !!this.dataChannel,
|
hasDataChannel: !!this.dataChannel,
|
||||||
dataChannelReady: this.dataChannel?.readyState === 'open',
|
dataChannelReady: this.dataChannel?.readyState === 'open',
|
||||||
|
|||||||
+165
-17
@@ -95,6 +95,18 @@ class PWAOfflineManager {
|
|||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
this.offlineDB = request.result;
|
this.offlineDB = request.result;
|
||||||
|
|
||||||
|
// Listen for database close events
|
||||||
|
this.offlineDB.onclose = () => {
|
||||||
|
console.log('🔒 IndexedDB connection closed');
|
||||||
|
this.offlineDB = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for database errors
|
||||||
|
this.offlineDB.onerror = (event) => {
|
||||||
|
console.error('❌ IndexedDB error:', event);
|
||||||
|
};
|
||||||
|
|
||||||
resolve(this.offlineDB);
|
resolve(this.offlineDB);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,6 +134,37 @@ class PWAOfflineManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure database is open, reopen if necessary
|
||||||
|
*/
|
||||||
|
async ensureDatabaseOpen() {
|
||||||
|
// Check if database exists and is open
|
||||||
|
if (this.offlineDB && this.offlineDB.objectStoreNames.length > 0) {
|
||||||
|
// Check if database connection is still valid
|
||||||
|
try {
|
||||||
|
// Try to access objectStoreNames to verify connection is active
|
||||||
|
const storeNames = this.offlineDB.objectStoreNames;
|
||||||
|
if (storeNames.length > 0) {
|
||||||
|
return; // Database is open and valid
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Database connection is invalid, need to reopen
|
||||||
|
console.warn('⚠️ Database connection invalid, reopening...');
|
||||||
|
this.offlineDB = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database is closed or invalid, reopen it
|
||||||
|
if (!this.offlineDB) {
|
||||||
|
try {
|
||||||
|
await this.initOfflineDB();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to reopen database:', error);
|
||||||
|
throw new Error('Database unavailable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Network status changes
|
// Network status changes
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
@@ -345,12 +388,6 @@ class PWAOfflineManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async queueOfflineAction(action) {
|
async queueOfflineAction(action) {
|
||||||
if (!this.offlineDB) {
|
|
||||||
console.warn('⚠️ Offline database not available');
|
|
||||||
this.offlineQueue.push(action);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueItem = {
|
const queueItem = {
|
||||||
...action,
|
...action,
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
@@ -360,21 +397,32 @@ class PWAOfflineManager {
|
|||||||
maxRetries: action.maxRetries || 3
|
maxRetries: action.maxRetries || 3
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Always add to memory queue as fallback
|
||||||
|
this.offlineQueue.push(queueItem);
|
||||||
|
|
||||||
|
if (!this.offlineDB) {
|
||||||
|
console.warn('⚠️ Offline database not available, using memory queue only');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
|
||||||
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
||||||
const store = transaction.objectStore('offlineQueue');
|
const store = transaction.objectStore('offlineQueue');
|
||||||
await this.promisifyRequest(store.add(queueItem));
|
await this.promisifyRequest(store.add(queueItem));
|
||||||
|
|
||||||
this.offlineQueue.push(queueItem);
|
|
||||||
|
|
||||||
// Try to register background sync
|
// Try to register background sync
|
||||||
if (this.registration) {
|
if (this.registration) {
|
||||||
await this.registration.sync.register('offline-sync');
|
await this.registration.sync.register('offline-sync');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
console.warn('⚠️ Database was closing, item added to memory queue only');
|
||||||
|
} else {
|
||||||
console.error('❌ Failed to queue offline action:', error);
|
console.error('❌ Failed to queue offline action:', error);
|
||||||
// Fallback to memory queue
|
}
|
||||||
this.offlineQueue.push(queueItem);
|
// Item already in memory queue, so no action needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,9 +439,32 @@ class PWAOfflineManager {
|
|||||||
try {
|
try {
|
||||||
// Process database queue
|
// Process database queue
|
||||||
if (this.offlineDB) {
|
if (this.offlineDB) {
|
||||||
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
// Ensure database is open before processing
|
||||||
const store = transaction.objectStore('offlineQueue');
|
await this.ensureDatabaseOpen();
|
||||||
const allItems = await this.promisifyRequest(store.getAll());
|
|
||||||
|
// Check if database is still open
|
||||||
|
if (!this.offlineDB || this.offlineDB.objectStoreNames.length === 0) {
|
||||||
|
console.warn('⚠️ Database not available, skipping queue processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all items first in a single transaction
|
||||||
|
let allItems = [];
|
||||||
|
try {
|
||||||
|
const readTransaction = this.offlineDB.transaction(['offlineQueue'], 'readonly');
|
||||||
|
const readStore = readTransaction.objectStore('offlineQueue');
|
||||||
|
allItems = await this.promisifyRequest(readStore.getAll());
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
console.warn('⚠️ Database was closing during read, retrying...');
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
const retryTransaction = this.offlineDB.transaction(['offlineQueue'], 'readonly');
|
||||||
|
const retryStore = retryTransaction.objectStore('offlineQueue');
|
||||||
|
allItems = await this.promisifyRequest(retryStore.getAll());
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by priority and timestamp
|
// Sort by priority and timestamp
|
||||||
allItems.sort((a, b) => {
|
allItems.sort((a, b) => {
|
||||||
@@ -403,10 +474,17 @@ class PWAOfflineManager {
|
|||||||
return a.timestamp - b.timestamp; // Older first
|
return a.timestamp - b.timestamp; // Older first
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process each item with its own transaction to avoid "database closing" errors
|
||||||
for (const item of allItems) {
|
for (const item of allItems) {
|
||||||
try {
|
try {
|
||||||
await this.processQueueItem(item);
|
await this.processQueueItem(item);
|
||||||
await this.promisifyRequest(store.delete(item.id));
|
|
||||||
|
// Create a new transaction for each delete operation
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
const deleteTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
||||||
|
const deleteStore = deleteTransaction.objectStore('offlineQueue');
|
||||||
|
await this.promisifyRequest(deleteStore.delete(item.id));
|
||||||
|
|
||||||
processedCount++;
|
processedCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to process offline action:', error);
|
console.error('❌ Failed to process offline action:', error);
|
||||||
@@ -417,11 +495,25 @@ class PWAOfflineManager {
|
|||||||
|
|
||||||
if (item.retryCount >= item.maxRetries) {
|
if (item.retryCount >= item.maxRetries) {
|
||||||
// Max retries reached, remove from queue
|
// Max retries reached, remove from queue
|
||||||
await this.promisifyRequest(store.delete(item.id));
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
const removeTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
||||||
|
const removeStore = removeTransaction.objectStore('offlineQueue');
|
||||||
|
await this.promisifyRequest(removeStore.delete(item.id));
|
||||||
console.log('❌ Max retries reached for action:', item.type);
|
console.log('❌ Max retries reached for action:', item.type);
|
||||||
|
} catch (removeError) {
|
||||||
|
console.error('❌ Failed to remove item after max retries:', removeError);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update retry count in database
|
// Update retry count in database
|
||||||
await this.promisifyRequest(store.put(item));
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
const updateTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
||||||
|
const updateStore = updateTransaction.objectStore('offlineQueue');
|
||||||
|
await this.promisifyRequest(updateStore.put(item));
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error('❌ Failed to update retry count:', updateError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,6 +732,8 @@ class PWAOfflineManager {
|
|||||||
if (!this.offlineDB) return;
|
if (!this.offlineDB) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
|
||||||
const appState = {
|
const appState = {
|
||||||
component: 'app_state',
|
component: 'app_state',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -669,9 +763,13 @@ class PWAOfflineManager {
|
|||||||
await this.promisifyRequest(store.put(appState));
|
await this.promisifyRequest(store.put(appState));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
console.warn('⚠️ Database was closing, could not save application state');
|
||||||
|
} else {
|
||||||
console.error('❌ Failed to save application state:', error);
|
console.error('❌ Failed to save application state:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async restoreApplicationState() {
|
async restoreApplicationState() {
|
||||||
if (!this.offlineDB) return null;
|
if (!this.offlineDB) return null;
|
||||||
@@ -695,9 +793,20 @@ class PWAOfflineManager {
|
|||||||
throw new Error('Offline database not available');
|
throw new Error('Offline database not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
||||||
const store = transaction.objectStore(storeName);
|
const store = transaction.objectStore(storeName);
|
||||||
return this.promisifyRequest(store.put(data));
|
return await this.promisifyRequest(store.put(data));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
const retryTransaction = this.offlineDB.transaction([storeName], 'readwrite');
|
||||||
|
const retryStore = retryTransaction.objectStore(storeName);
|
||||||
|
return await this.promisifyRequest(retryStore.put(data));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStoredData(storeName, key) {
|
async getStoredData(storeName, key) {
|
||||||
@@ -706,11 +815,24 @@ class PWAOfflineManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
const transaction = this.offlineDB.transaction([storeName], 'readonly');
|
const transaction = this.offlineDB.transaction([storeName], 'readonly');
|
||||||
const store = transaction.objectStore(storeName);
|
const store = transaction.objectStore(storeName);
|
||||||
const result = await this.promisifyRequest(store.get(key));
|
const result = await this.promisifyRequest(store.get(key));
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
console.warn(`⚠️ Database was closing during get from ${storeName}, retrying...`);
|
||||||
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
const retryTransaction = this.offlineDB.transaction([storeName], 'readonly');
|
||||||
|
const retryStore = retryTransaction.objectStore(storeName);
|
||||||
|
return await this.promisifyRequest(retryStore.get(key));
|
||||||
|
} catch (retryError) {
|
||||||
|
console.error(`❌ Failed to get stored data from ${storeName} after retry:`, retryError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
console.error(`❌ Failed to get stored data from ${storeName}:`, error);
|
console.error(`❌ Failed to get stored data from ${storeName}:`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -720,6 +842,7 @@ class PWAOfflineManager {
|
|||||||
if (!this.offlineDB) return;
|
if (!this.offlineDB) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
||||||
const store = transaction.objectStore(storeName);
|
const store = transaction.objectStore(storeName);
|
||||||
|
|
||||||
@@ -730,9 +853,13 @@ class PWAOfflineManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
console.warn(`⚠️ Database was closing during clear from ${storeName}`);
|
||||||
|
} else {
|
||||||
console.error(`❌ Failed to clear stored data from ${storeName}:`, error);
|
console.error(`❌ Failed to clear stored data from ${storeName}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerBackgroundSync() {
|
registerBackgroundSync() {
|
||||||
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
|
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
|
||||||
@@ -758,6 +885,8 @@ class PWAOfflineManager {
|
|||||||
const cutoffTime = Date.now() - maxAge;
|
const cutoffTime = Date.now() - maxAge;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.ensureDatabaseOpen();
|
||||||
|
|
||||||
const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite');
|
const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite');
|
||||||
|
|
||||||
// Clean offline queue
|
// Clean offline queue
|
||||||
@@ -790,9 +919,13 @@ class PWAOfflineManager {
|
|||||||
|
|
||||||
console.log('🧹 Old offline data cleaned up');
|
console.log('🧹 Old offline data cleaned up');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
|
||||||
|
console.warn('⚠️ Database was closing during cleanup, skipping...');
|
||||||
|
} else {
|
||||||
console.error('❌ Failed to cleanup old data:', error);
|
console.error('❌ Failed to cleanup old data:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleOfflineDisconnection(details) {
|
handleOfflineDisconnection(details) {
|
||||||
|
|
||||||
@@ -1030,11 +1163,26 @@ class PWAOfflineManager {
|
|||||||
destroy() {
|
destroy() {
|
||||||
if (this.reconnectInterval) {
|
if (this.reconnectInterval) {
|
||||||
clearInterval(this.reconnectInterval);
|
clearInterval(this.reconnectInterval);
|
||||||
|
this.reconnectInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set sync flag to prevent new operations
|
||||||
|
this.syncInProgress = true;
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
if (this.offlineDB) {
|
if (this.offlineDB) {
|
||||||
|
try {
|
||||||
|
// Only close if database is not in a transaction
|
||||||
|
// IndexedDB will automatically close when all transactions complete
|
||||||
|
if (this.offlineDB.objectStoreNames.length > 0) {
|
||||||
this.offlineDB.close();
|
this.offlineDB.close();
|
||||||
}
|
}
|
||||||
|
this.offlineDB = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Error closing database:', error);
|
||||||
|
this.offlineDB = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🧹 Offline Manager destroyed');
|
console.log('🧹 Offline Manager destroyed');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// SecureBit.chat Service Worker
|
// SecureBit.chat Service Worker
|
||||||
// Conservative PWA Edition v4.4.99 - Minimal Caching Strategy
|
// Conservative PWA Edition v4.5.22 - Minimal Caching Strategy
|
||||||
|
|
||||||
const CACHE_NAME = 'securebit-pwa-v4.4.99';
|
const CACHE_NAME = 'securebit-pwa-v4.5.22';
|
||||||
const STATIC_CACHE = 'securebit-pwa-static-v4.4.99';
|
const STATIC_CACHE = 'securebit-pwa-static-v4.5.22';
|
||||||
const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.4.99';
|
const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.5.22';
|
||||||
|
|
||||||
// Essential files for PWA offline functionality
|
// Essential files for PWA offline functionality
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user