Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b6431a36b | ||
|
|
ee635da66f | ||
|
|
92907732bf | ||
|
|
968fff9c61 | ||
|
|
79c8d08782 | ||
|
|
207e51361c | ||
|
|
4583db39a2 | ||
|
|
77ed4b3e4f | ||
|
|
c7b16157fc | ||
|
|
3c2bac588c | ||
|
|
2ae65eb749 | ||
|
|
4a3249b2a6 | ||
|
|
456545f055 | ||
|
|
40381cc0a1 | ||
|
|
0d7835cfa2 | ||
|
|
7604381584 | ||
|
|
a7a5861e0a | ||
|
|
63a19e6a4c | ||
|
|
b18e943abd | ||
|
|
9c1258cd1e | ||
|
|
60e4bb6b8a | ||
|
|
4233ba3d7e | ||
|
|
333c4b87b5 | ||
|
|
5096a09811 | ||
|
|
5c658cdd6c | ||
|
|
e209d3e6f5 | ||
|
|
49e5a054cb | ||
|
|
9d0372484a | ||
|
|
69fa5ffc28 | ||
|
|
ed169b4c8c | ||
|
|
906562333e | ||
|
|
4e7f5867b5 | ||
|
|
5ddfd1f5b3 | ||
|
|
1acbc12a92 | ||
|
|
3ed8766fc9 | ||
|
|
b087adfecc | ||
|
|
5b5cc67fdc | ||
|
|
93e4b7ad19 | ||
|
|
fce18d27a5 | ||
|
|
03e06f59dc | ||
|
|
5df6cb0d16 | ||
|
|
3730f46a08 | ||
|
|
abd6f9a7f5 | ||
|
|
99469e5bd5 | ||
|
|
2306b32482 | ||
|
|
cbb6a8fa31 | ||
|
|
4be6fc55f5 | ||
|
|
cb62d8eb3b | ||
|
|
ea8cf47118 | ||
|
|
d292c84829 | ||
|
|
45de8ce285 | ||
|
|
7af8f528ff | ||
|
|
d24d578321 | ||
|
|
dd9f13ab59 | ||
|
|
2e5b078bc2 | ||
|
|
75fa1cd27d | ||
|
|
60db79d2ae |
No files matched your search
@@ -0,0 +1,32 @@
|
||||
name: "CodeQL Analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript,typescript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Mirror to Codeberg
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.CODEBERG_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan codeberg.org >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Push to Codeberg
|
||||
run: |
|
||||
git remote remove codeberg || true
|
||||
git remote add codeberg git@codeberg.org:SecureBitChat/securebit-chat.git
|
||||
git push codeberg main --force
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Mirror to PrivacyGuides
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push to PrivacyGuides (HTTPS)
|
||||
env:
|
||||
PRIVACYGUIDES_TOKEN: ${{ secrets.PRIVACYGUIDES_TOKEN }}
|
||||
run: |
|
||||
git remote remove privacyguides || true
|
||||
git remote add privacyguides https://SecureBitChat:${PRIVACYGUIDES_TOKEN}@code.privacyguides.dev/SecureBitChat/securebit-chat.git
|
||||
git push privacyguides main --force
|
||||
#New
|
||||
@@ -19,6 +19,7 @@ SecureBit.chat is built with security-first principles and implements **military
|
||||
- **Enhanced Replay Protection:** Multi-factor protection with sequence numbers, message IDs, and timestamps
|
||||
- **Secure Key Storage:** WeakMap-based isolation preventing direct access to sensitive keys
|
||||
- **Key Security Monitoring:** Automatic validation, rotation, and emergency wipe capabilities
|
||||
- **HKDF Key Derivation:** RFC 5869 compliant key separation with proper salt and info parameters
|
||||
|
||||
### Advanced Traffic Obfuscation
|
||||
- **Packet Padding:** Random padding (64-512 bytes) to hide real message sizes
|
||||
@@ -116,7 +117,7 @@ We maintain a hall of fame for security researchers who help improve SecureBit.c
|
||||
## 📊 Security Architecture (Stage 5)
|
||||
|
||||
```
|
||||
18-Layer Security Architecture:
|
||||
19-Layer Security Architecture:
|
||||
├── Layer 1: Enhanced Authentication (ECDSA P-384 + SHA-384)
|
||||
├── Layer 2: Key Exchange (ECDH P-384, non-extractable keys)
|
||||
├── Layer 3: Metadata Protection (AES-256-GCM + 64-byte salt)
|
||||
@@ -134,7 +135,8 @@ We maintain a hall of fame for security researchers who help improve SecureBit.c
|
||||
├── Layer 15: Production Logging (Data sanitization)
|
||||
├── Layer 16: ASN.1 Validation (Complete key structure verification)
|
||||
├── Layer 17: OID Validation (Algorithm and curve verification)
|
||||
└── Layer 18: EC Point Validation (Format and structure verification)
|
||||
├── Layer 18: EC Point Validation (Format and structure verification)
|
||||
└── Layer 19: HKDF Key Derivation (RFC 5869 compliant key separation)
|
||||
```
|
||||
|
||||
### Security Metrics
|
||||
@@ -202,7 +204,7 @@ We maintain a hall of fame for security researchers who help improve SecureBit.c
|
||||
## 🔄 Recent Security Updates (Version 4.02)
|
||||
|
||||
### Major Security Enhancements:
|
||||
- ✅ **Implemented 18-layer security architecture**
|
||||
- ✅ **Implemented 19-layer security architecture**
|
||||
- ✅ **Added complete ASN.1 DER parser for key validation**
|
||||
- ✅ **Enhanced key security with OID and EC point verification**
|
||||
- ✅ **Fixed high-risk vulnerability in key structure validation**
|
||||
@@ -210,6 +212,8 @@ We maintain a hall of fame for security researchers who help improve SecureBit.c
|
||||
- ✅ **Implemented key size limits to prevent DoS attacks**
|
||||
- ✅ **Added BIT STRING validation ensuring unused bits are 0**
|
||||
- ✅ **Enhanced fallback support from P-384 to P-256**
|
||||
- ✅ **Implemented RFC 5869 compliant HKDF key derivation**
|
||||
- ✅ **Enhanced key separation with proper salt and info parameters**
|
||||
|
||||
### Previous Enhancements (Version 4.01):
|
||||
- ✅ **Implemented 15-layer security architecture**
|
||||
@@ -266,13 +270,14 @@ cryptoManager.getASN1ValidationStatus()
|
||||
## 🏅 Security Achievements
|
||||
|
||||
SecureBit.chat v4.02 provides:
|
||||
- **🥇 Military-Grade Security:** 18-layer protection system
|
||||
- **🥇 Military-Grade Security:** 19-layer protection system
|
||||
- **🥇 Government-Level Encryption:** Triple AES-256-GCM + P-384 ECDH/ECDSA
|
||||
- **🥇 Perfect Forward Secrecy:** Complete with automatic key rotation
|
||||
- **🥇 Traffic Analysis Protection:** Maximum with 6-layer obfuscation
|
||||
- **🥇 Zero-Trust Architecture:** No central points of failure
|
||||
- **🥇 Complete ASN.1 Validation:** Full structural verification of all cryptographic keys
|
||||
- **🥇 PKCS Compliance:** Complete adherence to cryptographic standards
|
||||
- **🥇 HKDF Key Derivation:** RFC 5869 compliant key separation and derivation
|
||||
|
||||
**Security Rating: MAXIMUM** - Exceeds most government and military communication standards with complete key structure validation.
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Notification Sound Asset
|
||||
#
|
||||
# This file should contain a short, pleasant notification sound in MP3 format.
|
||||
# Recommended specifications:
|
||||
# - Duration: 1-2 seconds
|
||||
# - Format: MP3, 44.1kHz, 128kbps
|
||||
# - Volume: Moderate (not too loud)
|
||||
# - License: Ensure proper licensing for commercial use
|
||||
#
|
||||
# You can create this using:
|
||||
# 1. Text-to-speech generators
|
||||
# 2. Audio editing software
|
||||
# 3. Free notification sound libraries
|
||||
# 4. AI-generated sounds
|
||||
#
|
||||
# Example sources:
|
||||
# - Freesound.org (CC licensed)
|
||||
# - Zapsplat.com
|
||||
# - Adobe Audition
|
||||
# - Audacity (free)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1 +1 @@
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media Line truncated
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@mediLine truncated
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/components/ui/SessionTimer.jsx", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx", "../src/scripts/app-boot.js"],
|
||||
"sourcesContent": ["class EnhancedSecureCryptoUtils {\r\n\r\n static _keyMetadata = new WeakMap();\r\n \r\n // Initialize secure logging system after class definition\r\n\r\n // Utility to sort object keys for deterministic serialization\r\n static sortObjectKeys(obj) {\r\n if (typeof obj !== 'object' || obj === null) {\r\n return obj;\r\n }\r\n\r\n if (Array.isArray(obj)) {\r\n return obj.map(EnhancedSecureCryptoUtils.sortObjectKeys);\r\n }\r\n\r\n const sortedObj = {};\r\n Object.keys(obj).sort().forEach(key => {\r\n sortedObj[key] = EnhancedSecureCryptoUtils.sortObjectKeys(obj[key]);\r\n });\r\n return sortedObj;\r\n }\r\n\r\n // Utility to assert CryptoKey type and properties\r\n static assertCryptoKey(key, expectedName = null, expectedUsages = []) {\r\n if (!(key instanceof CryptoKey)) throw new Error('Expected CryptoKey');\r\n if (expectedName && key.algorithm?.name !== expectedName) {\r\n throw new Error(`Expected algorithm ${expectedName}, got ${key.algorithm?.name}`);\r\n }\r\n for (const u of expectedUsages) {\r\n if (!key.usages || !key.usages.includes(u)) {\r\n throw new Error(`Missing required key usage: ${u}`);\r\n }\r\n }\r\n }\r\n // Helper function to convert ArrayBuffer to Base64\r\n static arrayBufferToBase64(buffer) {\r\n let binary = '';\r\n const bytes = new Uint8Array(buffer);\r\n const len = bytes.byteLength;\r\n for (let i = 0; i < len; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n }\r\n\r\n // Helper function to convert Base64 to ArrayBuffer\r\n static base64ToArrayBuffer(base64) {\r\n try {\r\n // Validate input\r\n if (typeof base64 !== 'string' || !base64) {\r\n throw new Error('Invalid base64 input: must be a non-empty string');\r\n }\r\n\r\n // Remove any whitespace and validate base64 format\r\n const cleanBase64 = base64.trim();\r\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {\r\n throw new Error('Invalid base64 format');\r\n }\r\n\r\n // Handle empty string case\r\n if (cleanBase64 === '') {\r\n return new ArrayBuffer(0);\r\n }\r\n\r\n const binaryString = atob(cleanBase64);\r\n const len = binaryString.length;\r\n const bytes = new Uint8Array(len);\r\n for (let i = 0; i < len; i++) {\r\n bytes[i] = binaryString.charCodeAt(i);\r\n }\r\n return bytes.buffer;\r\n } catch (error) {\r\n console.error('Base64 to ArrayBuffer conversion failed:', error.message);\r\n throw new Error(`Base64 conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n // Helper function to convert hex string to Uint8Array\r\n static hexToUint8Array(hexString) {\r\n try {\r\n if (!hexString || typeof hexString !== 'string') {\r\n throw new Error('Invalid hex string input: must be a non-empty string');\r\n }\r\n\r\n // Remove colons and spaces from hex string (e.g., \"aa:bb:cc\" -> \"aabbcc\")\r\n const cleanHex = hexString.replace(/:/g, '').replace(/\\s/g, '');\r\n \r\n // Validate hex format\r\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\r\n throw new Error('Invalid hex format: contains non-hex characters');\r\n }\r\n \r\n // Ensure even length\r\n if (cleanHex.length % 2 !== 0) {\r\n throw new Error('Invalid hex format: odd length');\r\n }\r\n\r\n // Convert hex string to bytes\r\n const bytes = new Uint8Array(cleanHex.length / 2);\r\n for (let i = 0; i < cleanHex.length; i += 2) {\r\n bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);\r\n }\r\n \r\n return bytes;\r\n } catch (error) {\r\n console.error('Hex to Uint8Array conversion failed:', error.message);\r\n throw new Error(`Hex conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n static async encryptData(data, password) {\r\n try {\r\n const dataString = typeof data === 'string' ? data : JSON.stringify(data);\r\n const salt = crypto.getRandomValues(new Uint8Array(16));\r\n const encoder = new TextEncoder();\r\n const passwordBuffer = encoder.encode(password);\r\n\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n passwordBuffer,\r\n { name: 'PBKDF2' },\r\n false,\r\n ['deriveKey']\r\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["fileMessageTypes", "attempts", "start", "error", "attempts", "chunk", "fileMessageTypes", "window", "start", "webrtcManager", "start"]
|
||||
"sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx"],
|
||||
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["NotificationIntegration", "fileMessageTypes", "attempts", "start", "error", "attempts", "chunk", "fileMessageTypes", "window", "start", "webrtcManager", "start"]
|
||||
}
|
||||
@@ -213,7 +213,7 @@ var EnhancedChatMessage = ({ message, type, timestamp }) => {
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
key: "message",
|
||||
className: "text-sm"
|
||||
className: "text-sm break-words whitespace-normal"
|
||||
}, message),
|
||||
timestamp && React.createElement("div", {
|
||||
key: "meta",
|
||||
@@ -267,11 +267,16 @@ var EnhancedConnectionSetup = ({
|
||||
toggleQrManualMode,
|
||||
nextQrFrame,
|
||||
prevQrFrame,
|
||||
markAnswerCreated
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef,
|
||||
isGeneratingKeys,
|
||||
handleCreateOffer
|
||||
}) => {
|
||||
const [mode, setMode] = React.useState("select");
|
||||
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||
const resetToSelect = () => {
|
||||
setMode("select");
|
||||
setIsGeneratingKeys(false);
|
||||
onClearData();
|
||||
};
|
||||
const handleVerificationConfirm = () => {
|
||||
@@ -280,6 +285,76 @@ var EnhancedConnectionSetup = ({
|
||||
const handleVerificationReject = () => {
|
||||
onVerifyConnection(false);
|
||||
};
|
||||
const requestNotificationPermissionOnInteraction = async () => {
|
||||
if (notificationPermissionRequested) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!("Notification" in window)) {
|
||||
return;
|
||||
}
|
||||
if (!window.isSecureContext && window.location.protocol !== "https:" && window.location.hostname !== "localhost") {
|
||||
return;
|
||||
}
|
||||
const currentPermission = typeof Notification !== "undefined" && Notification ? Notification.permission : "denied";
|
||||
if (currentPermission === "default" && typeof Notification !== "undefined" && Notification) {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission === "granted") {
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const welcomeNotification = new Notification("SecureBit Chat", {
|
||||
body: "Notifications enabled! You will receive alerts for new messages.",
|
||||
icon: "/logo/icon-192x192.png",
|
||||
tag: "welcome-notification"
|
||||
});
|
||||
welcomeNotification.onclick = () => {
|
||||
welcomeNotification.close();
|
||||
};
|
||||
setTimeout(() => {
|
||||
welcomeNotification.close();
|
||||
}, 5e3);
|
||||
} catch (error) {
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
} else if (currentPermission === "granted") {
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const testNotification = new Notification("SecureBit Chat", {
|
||||
body: "Notifications are working! You will receive alerts for new messages.",
|
||||
icon: "/logo/icon-192x192.png",
|
||||
tag: "test-notification"
|
||||
});
|
||||
testNotification.onclick = () => {
|
||||
testNotification.close();
|
||||
};
|
||||
setTimeout(() => {
|
||||
testNotification.close();
|
||||
}, 5e3);
|
||||
} catch (error) {
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
setNotificationPermissionRequested(true);
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
if (showVerification) {
|
||||
return React.createElement("div", {
|
||||
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
||||
@@ -327,7 +402,15 @@ var EnhancedConnectionSetup = ({
|
||||
// Create Connection
|
||||
React.createElement("div", {
|
||||
key: "create",
|
||||
onClick: () => setMode("create"),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode("create");
|
||||
setTimeout(() => {
|
||||
if (webrtcManagerRef.current) {
|
||||
handleCreateOffer();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
@@ -399,7 +482,10 @@ var EnhancedConnectionSetup = ({
|
||||
// Join Connection
|
||||
React.createElement("div", {
|
||||
key: "join",
|
||||
onClick: () => setMode("join"),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode("join");
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
@@ -510,16 +596,15 @@ var EnhancedConnectionSetup = ({
|
||||
key: "description",
|
||||
className: "text-secondary text-sm mb-4"
|
||||
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
||||
!showOfferStep && React.createElement("button", {
|
||||
key: "create-btn",
|
||||
onClick: onCreateOffer,
|
||||
disabled: connectionStatus === "connecting",
|
||||
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
||||
!showOfferStep && isGeneratingKeys && React.createElement("div", {
|
||||
key: "loading-state",
|
||||
className: "w-full py-3 px-4 rounded-lg font-medium transition-all duration-200 bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center"
|
||||
}, [
|
||||
React.createElement("i", {
|
||||
className: "fas fa-shield-alt mr-2"
|
||||
key: "spinner",
|
||||
className: "fas fa-spinner fa-spin mr-2"
|
||||
}),
|
||||
"Create secure keys"
|
||||
"Generating secure keys..."
|
||||
]),
|
||||
showOfferStep && React.createElement("div", {
|
||||
key: "offer-result",
|
||||
@@ -622,58 +707,6 @@ var EnhancedConnectionSetup = ({
|
||||
])
|
||||
])
|
||||
]),
|
||||
// Step 2 - Session Type Selection
|
||||
// showOfferStep && React.createElement('div', {
|
||||
// key: 'step2',
|
||||
// className: "card-minimal rounded-xl p-6"
|
||||
// }, [
|
||||
// React.createElement('div', {
|
||||
// key: 'step-header',
|
||||
// className: "flex items-center mb-4"
|
||||
// }, [
|
||||
// React.createElement('div', {
|
||||
// key: 'number',
|
||||
// className: "w-8 h-8 bg-green-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
||||
// }, '2'),
|
||||
// React.createElement('h3', {
|
||||
// key: 'title',
|
||||
// className: "text-lg font-medium text-primary"
|
||||
// }, "Select session type")
|
||||
// ]),
|
||||
// React.createElement('p', {
|
||||
// key: 'description',
|
||||
// className: "text-secondary text-sm mb-4"
|
||||
// }, "Choose a session plan or use limited demo mode for testing."),
|
||||
// React.createElement(SessionTypeSelector, {
|
||||
// key: 'session-selector',
|
||||
// onSelectType: (sessionType) => {
|
||||
// // Save the selected session type
|
||||
// setSelectedSessionType(sessionType);
|
||||
// console.log('🎯 Session type selected:', sessionType);
|
||||
// // FIX: For demo sessions, we immediately call automatic activation
|
||||
// if (sessionType === 'demo') {
|
||||
// console.log('🎮 Demo session selected, scheduling automatic activation...');
|
||||
// // Delay activation for 2 seconds to stabilize
|
||||
// setTimeout(() => {
|
||||
// if (sessionManager) {
|
||||
// console.log('🚀 Triggering demo session activation from selection...');
|
||||
// handleDemoVerification();
|
||||
// }
|
||||
// }, 2000);
|
||||
// }
|
||||
// // Open a modal payment window
|
||||
// if (typeof window.showPaymentModal === 'function') {
|
||||
// window.showPaymentModal(sessionType);
|
||||
// } else {
|
||||
// // Fallback - show session information
|
||||
// console.log('Selected session type:', sessionType);
|
||||
// }
|
||||
// },
|
||||
// onCancel: resetToSelect,
|
||||
// sessionManager: window.sessionManager
|
||||
// })
|
||||
// ]),
|
||||
// Step 3 - Waiting for response
|
||||
showOfferStep && React.createElement("div", {
|
||||
key: "step2",
|
||||
className: "card-minimal rounded-xl p-6"
|
||||
@@ -714,9 +747,9 @@ var EnhancedConnectionSetup = ({
|
||||
React.createElement("textarea", {
|
||||
key: "input",
|
||||
value: answerInput,
|
||||
onChange: (e2) => {
|
||||
setAnswerInput(e2.target.value);
|
||||
if (e2.target.value.trim().length > 0) {
|
||||
onChange: (e) => {
|
||||
setAnswerInput(e.target.value);
|
||||
if (e.target.value.trim().length > 0) {
|
||||
if (typeof markAnswerCreated === "function") {
|
||||
markAnswerCreated();
|
||||
}
|
||||
@@ -792,9 +825,9 @@ var EnhancedConnectionSetup = ({
|
||||
React.createElement("textarea", {
|
||||
key: "input",
|
||||
value: offerInput,
|
||||
onChange: (e2) => {
|
||||
setOfferInput(e2.target.value);
|
||||
if (e2.target.value.trim().length > 0) {
|
||||
onChange: (e) => {
|
||||
setOfferInput(e.target.value);
|
||||
if (e.target.value.trim().length > 0) {
|
||||
if (typeof markAnswerCreated === "function") {
|
||||
markAnswerCreated();
|
||||
}
|
||||
@@ -1085,9 +1118,9 @@ var EnhancedChatInterface = ({
|
||||
setShowScrollButton(false);
|
||||
}
|
||||
};
|
||||
const handleKeyPress = (e2) => {
|
||||
if (e2.key === "Enter" && !e2.shiftKey) {
|
||||
e2.preventDefault();
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSendMessage();
|
||||
}
|
||||
};
|
||||
@@ -1272,7 +1305,7 @@ var EnhancedChatInterface = ({
|
||||
[
|
||||
React.createElement("textarea", {
|
||||
value: messageInput,
|
||||
onChange: (e2) => setMessageInput(e2.target.value),
|
||||
onChange: (e) => setMessageInput(e.target.value),
|
||||
onKeyDown: handleKeyPress,
|
||||
placeholder: "Enter message to encrypt...",
|
||||
rows: 2,
|
||||
@@ -1332,12 +1365,13 @@ var EnhancedSecureP2PChat = () => {
|
||||
const [qrCodeUrl, setQrCodeUrl] = React.useState("");
|
||||
const [showQRScanner, setShowQRScanner] = React.useState(false);
|
||||
const [showQRScannerModal, setShowQRScannerModal] = React.useState(false);
|
||||
const [isGeneratingKeys, setIsGeneratingKeys2] = React.useState(false);
|
||||
const [isVerified, setIsVerified] = React.useState(false);
|
||||
const [securityLevel, setSecurityLevel] = React.useState(null);
|
||||
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
||||
const [localVerificationConfirmed, setLocalVerificationConfirmed] = React.useState(false);
|
||||
const [remoteVerificationConfirmed, setRemoteVerificationConfirmed] = React.useState(false);
|
||||
const [bothVerificationsConfirmed, setBothVerificationsConfirmed] = React.useState(false);
|
||||
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
||||
const [pendingSession, setPendingSession] = React.useState(null);
|
||||
const [connectionState, setConnectionState] = React.useState({
|
||||
status: "disconnected",
|
||||
@@ -1359,7 +1393,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
const now = Date.now();
|
||||
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
||||
const maxPreserveTime = 3e5;
|
||||
const hasAnswerData = answerData && answerData.trim().length > 0 || answerInput && answerInput.trim().length > 0;
|
||||
const hasAnswerData = answerData && typeof answerData === "string" && answerData.trim().length > 0 || answerInput && answerInput.trim().length > 0;
|
||||
const hasAnswerQR = qrCodeUrl && qrCodeUrl.trim().length > 0;
|
||||
const shouldPreserve = connectionState.hasActiveAnswer && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerData && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerQR && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect;
|
||||
return shouldPreserve;
|
||||
@@ -1373,8 +1407,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
React.useEffect(() => {
|
||||
window.forceCleanup = () => {
|
||||
handleClearData();
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
};
|
||||
window.clearLogs = () => {
|
||||
@@ -1387,8 +1421,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
delete window.clearLogs;
|
||||
};
|
||||
}, []);
|
||||
const webrtcManagerRef = React.useRef(null);
|
||||
window.webrtcManagerRef = webrtcManagerRef;
|
||||
const webrtcManagerRef2 = React.useRef(null);
|
||||
const notificationIntegrationRef = React.useRef(null);
|
||||
window.webrtcManagerRef = webrtcManagerRef2;
|
||||
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
||||
const newMessage = {
|
||||
message,
|
||||
@@ -1429,7 +1464,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
window.isUpdatingSecurity = true;
|
||||
try {
|
||||
if (webrtcManagerRef.current) {
|
||||
if (webrtcManagerRef2.current) {
|
||||
setSecurityLevel({
|
||||
level: "MAXIMUM",
|
||||
score: 100,
|
||||
@@ -1440,7 +1475,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isRealData: true
|
||||
});
|
||||
if (window.DEBUG_MODE) {
|
||||
const currentLevel = webrtcManagerRef.current.ecdhKeyPair && webrtcManagerRef.current.ecdsaKeyPair ? await webrtcManagerRef.current.calculateSecurityLevel() : {
|
||||
const currentLevel = webrtcManagerRef2.current.ecdhKeyPair && webrtcManagerRef2.current.ecdsaKeyPair ? await webrtcManagerRef2.current.calculateSecurityLevel() : {
|
||||
level: "MAXIMUM",
|
||||
score: 100,
|
||||
sessionType: "premium",
|
||||
@@ -1463,12 +1498,6 @@ var EnhancedSecureP2PChat = () => {
|
||||
}, 2e3);
|
||||
}
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setSessionTimeLeft(0);
|
||||
}, 1e3);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
const chatMessagesRef = React.useRef(null);
|
||||
const scrollToBottom = createScrollToBottomFunction(chatMessagesRef);
|
||||
React.useEffect(() => {
|
||||
@@ -1479,7 +1508,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
}, [messages]);
|
||||
React.useEffect(() => {
|
||||
if (webrtcManagerRef.current) {
|
||||
if (webrtcManagerRef2.current) {
|
||||
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
@@ -1554,8 +1583,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
setLocalVerificationConfirmed(false);
|
||||
setRemoteVerificationConfirmed(false);
|
||||
setBothVerificationsConfirmed(false);
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
@@ -1563,12 +1592,11 @@ var EnhancedSecureP2PChat = () => {
|
||||
setKeyFingerprint("");
|
||||
setVerificationCode("");
|
||||
setSecurityLevel(null);
|
||||
setSessionTimeLeft(0);
|
||||
setTimeout(() => {
|
||||
setConnectionStatus("disconnected");
|
||||
setShowVerification(false);
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
@@ -1588,8 +1616,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
setLocalVerificationConfirmed(false);
|
||||
setRemoteVerificationConfirmed(false);
|
||||
setBothVerificationsConfirmed(false);
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
@@ -1642,7 +1670,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (typeof console.clear === "function") {
|
||||
console.clear();
|
||||
}
|
||||
webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
|
||||
webrtcManagerRef2.current = new EnhancedSecureWebRTCManager(
|
||||
handleMessage,
|
||||
handleStatusChange,
|
||||
handleKeyExchange,
|
||||
@@ -1650,12 +1678,22 @@ var EnhancedSecureP2PChat = () => {
|
||||
handleAnswerError,
|
||||
handleVerificationStateChange
|
||||
);
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.3.120 - 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");
|
||||
if (typeof Notification !== "undefined" && Notification && Notification.permission === "granted" && window.NotificationIntegration && !notificationIntegrationRef.current) {
|
||||
try {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef2.current);
|
||||
integration.init().then(() => {
|
||||
notificationIntegrationRef.current = integration;
|
||||
}).catch((error) => {
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.7.53 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (event.type === "beforeunload" && !isTabSwitching) {
|
||||
if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
|
||||
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
|
||||
try {
|
||||
webrtcManagerRef.current.sendSystemMessage({
|
||||
webrtcManagerRef2.current.sendSystemMessage({
|
||||
type: "peer_disconnect",
|
||||
reason: "user_disconnect",
|
||||
timestamp: Date.now()
|
||||
@@ -1663,12 +1701,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (error) {
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
}, 100);
|
||||
} else if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
} else if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
} else if (isTabSwitching) {
|
||||
event.preventDefault();
|
||||
@@ -1696,8 +1734,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.setFileTransferCallbacks(
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.setFileTransferCallbacks(
|
||||
// Progress callback
|
||||
(progress) => {
|
||||
console.log("File progress:", progress);
|
||||
@@ -1720,9 +1758,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
a.download = fileData.fileName;
|
||||
a.click();
|
||||
setTimeout(() => fileData.revokeObjectURL(url), 15e3);
|
||||
} catch (e2) {
|
||||
console.error("Download failed:", e2);
|
||||
addMessageWithAutoScroll(` File upload error: ${String(e2?.message || e2)}`, "system");
|
||||
} catch (e) {
|
||||
console.error("Download failed:", e);
|
||||
addMessageWithAutoScroll(` File upload error: ${String(e?.message || e)}`, "system");
|
||||
}
|
||||
}
|
||||
}, "Download")
|
||||
@@ -1749,9 +1787,9 @@ var EnhancedSecureP2PChat = () => {
|
||||
clearTimeout(tabSwitchTimeout);
|
||||
tabSwitchTimeout = null;
|
||||
}
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
webrtcManagerRef.current = null;
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
webrtcManagerRef2.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -1838,8 +1876,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
const QR_SIZE = isDesktop ? 720 : 512;
|
||||
const url = await (window.generateQRCode ? window.generateQRCode(current, { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE }) : Promise.resolve(""));
|
||||
if (url) setQrCodeUrl(url);
|
||||
} catch (e2) {
|
||||
console.warn("Animated QR render error (current):", e2);
|
||||
} catch (e) {
|
||||
console.warn("Animated QR render error (current):", e);
|
||||
}
|
||||
setQrFrameIndex((qrAnimationRef.current?.idx || 0) % (qrAnimationRef.current?.chunks?.length || 1) + 1);
|
||||
};
|
||||
@@ -1945,8 +1983,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
setQrFrameIndex(1);
|
||||
return;
|
||||
}
|
||||
} catch (e2) {
|
||||
console.warn("Binary QR generation failed, falling back to compressed:", e2?.message || e2);
|
||||
} catch (e) {
|
||||
console.warn("Binary QR generation failed, falling back to compressed:", e?.message || e);
|
||||
}
|
||||
}
|
||||
if (typeof window.generateCompressedQRCode === "function") {
|
||||
@@ -1969,8 +2007,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
setQrFrameIndex(1);
|
||||
return;
|
||||
}
|
||||
} catch (e2) {
|
||||
console.warn("Compressed QR generation failed, falling back to plain:", e2?.message || e2);
|
||||
} catch (e) {
|
||||
console.warn("Compressed QR generation failed, falling back to plain:", e?.message || e);
|
||||
}
|
||||
}
|
||||
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
||||
@@ -2164,8 +2202,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
|
||||
setShowQRScannerModal(false);
|
||||
return Promise.resolve(true);
|
||||
} catch (e2) {
|
||||
console.warn("Binary chunks reconstruction failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("Binary chunks reconstruction failed:", e);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
@@ -2223,12 +2261,11 @@ var EnhancedSecureP2PChat = () => {
|
||||
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
|
||||
setShowQRScannerModal(false);
|
||||
return Promise.resolve(true);
|
||||
} catch (e2) {
|
||||
console.warn("Binary chunks reconstruction failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("Binary chunks reconstruction failed:", e);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
console.log("Processing single QR code:", scannedData.substring(0, 50) + "...");
|
||||
let parsedData;
|
||||
if (typeof window.decodeAnyPayload === "function") {
|
||||
const any = window.decodeAnyPayload(scannedData);
|
||||
@@ -2283,8 +2320,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
|
||||
setShowQRScannerModal(false);
|
||||
return Promise.resolve(true);
|
||||
} catch (e2) {
|
||||
console.warn("RAW multi-frame reconstruction failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("RAW multi-frame reconstruction failed:", e);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else if (hdr.rt === "bin") {
|
||||
@@ -2311,8 +2348,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
|
||||
setShowQRScannerModal(false);
|
||||
return Promise.resolve(true);
|
||||
} catch (e2) {
|
||||
console.warn("BIN multi-frame reconstruction failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("BIN multi-frame reconstruction failed:", e);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else if (window.receiveAndProcess) {
|
||||
@@ -2334,8 +2371,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
setShowQRScannerModal(false);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
} catch (e2) {
|
||||
console.warn("COSE multi-chunk processing failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("COSE multi-chunk processing failed:", e);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
} else {
|
||||
@@ -2417,11 +2454,12 @@ var EnhancedSecureP2PChat = () => {
|
||||
};
|
||||
const handleCreateOffer = async () => {
|
||||
try {
|
||||
setIsGeneratingKeys2(true);
|
||||
setOfferData("");
|
||||
setShowOfferStep(false);
|
||||
setShowQRCode(false);
|
||||
setQrCodeUrl("");
|
||||
const offer = await webrtcManagerRef.current.createSecureOffer();
|
||||
const offer = await webrtcManagerRef2.current.createSecureOffer();
|
||||
setOfferData(offer);
|
||||
setShowOfferStep(true);
|
||||
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
||||
@@ -2429,12 +2467,15 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||
const bin = window.encodeBinaryToPrefixed(offerString);
|
||||
const TARGET_CHUNKS = 4;
|
||||
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||
let total = TARGET_CHUNKS;
|
||||
let FRAME_MAX = Math.max(200, Math.ceil(bin.length / TARGET_CHUNKS));
|
||||
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||
if (total < 2) {
|
||||
total = 2;
|
||||
FRAME_MAX = Math.ceil(bin.length / 2) || 1;
|
||||
if (bin.length <= FRAME_MAX) {
|
||||
total = 1;
|
||||
FRAME_MAX = bin.length;
|
||||
} else {
|
||||
FRAME_MAX = Math.ceil(bin.length / TARGET_CHUNKS);
|
||||
total = TARGET_CHUNKS;
|
||||
}
|
||||
const id = `bin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const chunks = [];
|
||||
@@ -2472,8 +2513,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
} catch (e2) {
|
||||
console.warn("Offer QR generation failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("Offer QR generation failed:", e);
|
||||
}
|
||||
const existingMessages = messages.filter(
|
||||
(m) => m.type === "system" && (m.message.includes("Secure invitation created") || m.message.includes("Send the encrypted code"))
|
||||
@@ -2502,6 +2543,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} finally {
|
||||
setIsGeneratingKeys2(false);
|
||||
}
|
||||
};
|
||||
const handleCreateAnswer = async () => {
|
||||
@@ -2541,7 +2584,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!isValidOfferType) {
|
||||
throw new Error("Invalid invitation type. Expected offer or enhanced_secure_offer");
|
||||
}
|
||||
const answer = await webrtcManagerRef.current.createSecureAnswer(offer);
|
||||
const answer = await webrtcManagerRef2.current.createSecureAnswer(offer);
|
||||
setAnswerData(answer);
|
||||
setShowAnswerStep(true);
|
||||
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
||||
@@ -2549,12 +2592,15 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||
const bin = window.encodeBinaryToPrefixed(answerString);
|
||||
const TARGET_CHUNKS = 4;
|
||||
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||
let total = TARGET_CHUNKS;
|
||||
let FRAME_MAX = Math.max(200, Math.ceil(bin.length / TARGET_CHUNKS));
|
||||
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||
if (total < 2) {
|
||||
total = 2;
|
||||
FRAME_MAX = Math.ceil(bin.length / 2) || 1;
|
||||
if (bin.length <= FRAME_MAX) {
|
||||
total = 1;
|
||||
FRAME_MAX = bin.length;
|
||||
} else {
|
||||
FRAME_MAX = Math.ceil(bin.length / TARGET_CHUNKS);
|
||||
total = TARGET_CHUNKS;
|
||||
}
|
||||
const id = `ans_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const chunks = [];
|
||||
@@ -2592,10 +2638,10 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
} catch (e2) {
|
||||
console.warn("Answer QR generation failed:", e2);
|
||||
} catch (e) {
|
||||
console.warn("Answer QR generation failed:", e);
|
||||
}
|
||||
if (e.target.value.trim().length > 0) {
|
||||
if (answerInput.trim().length > 0) {
|
||||
if (typeof markAnswerCreated === "function") {
|
||||
markAnswerCreated();
|
||||
}
|
||||
@@ -2676,7 +2722,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") {
|
||||
throw new Error("Invalid response type. Expected answer or enhanced_secure_answer");
|
||||
}
|
||||
await webrtcManagerRef.current.handleSecureAnswer(answer);
|
||||
await webrtcManagerRef2.current.handleSecureAnswer(answer);
|
||||
if (pendingSession) {
|
||||
setPendingSession(null);
|
||||
setMessages((prev) => [...prev, {
|
||||
@@ -2762,10 +2808,37 @@ var EnhancedSecureP2PChat = () => {
|
||||
setConnectionStatus("failed");
|
||||
}
|
||||
};
|
||||
const handleVerifyConnection = (isValid) => {
|
||||
const handleVerifyConnection = async (isValid) => {
|
||||
if (isValid) {
|
||||
webrtcManagerRef.current.confirmVerification();
|
||||
webrtcManagerRef2.current.confirmVerification();
|
||||
setLocalVerificationConfirmed(true);
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef2.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef2.current);
|
||||
await integration.init();
|
||||
notificationIntegrationRef.current = integration;
|
||||
const status = integration.getStatus();
|
||||
if (status.permission === "granted") {
|
||||
setMessages((prev) => [...prev, {
|
||||
message: "\u2713 Notifications enabled - you will receive alerts when the tab is inactive",
|
||||
type: "system",
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} else {
|
||||
setMessages((prev) => [...prev, {
|
||||
message: "\u2139 Notifications disabled - you can enable them using the button on the main page",
|
||||
type: "system",
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
}
|
||||
} else if (notificationIntegrationRef.current) {
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize notifications:", error);
|
||||
}
|
||||
} else {
|
||||
setMessages((prev) => [...prev, {
|
||||
message: " Verification rejected. The connection is unsafe! Session reset..",
|
||||
@@ -2779,8 +2852,8 @@ var EnhancedSecureP2PChat = () => {
|
||||
setShowVerification(false);
|
||||
setVerificationCode("");
|
||||
setConnectionStatus("disconnected");
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
@@ -2799,15 +2872,15 @@ var EnhancedSecureP2PChat = () => {
|
||||
if (!messageInput.trim()) {
|
||||
return;
|
||||
}
|
||||
if (!webrtcManagerRef.current) {
|
||||
if (!webrtcManagerRef2.current) {
|
||||
return;
|
||||
}
|
||||
if (!webrtcManagerRef.current.isConnected()) {
|
||||
if (!webrtcManagerRef2.current.isConnected()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
addMessageWithAutoScroll(messageInput.trim(), "sent");
|
||||
await webrtcManagerRef.current.sendMessage(messageInput);
|
||||
await webrtcManagerRef2.current.sendMessage(messageInput);
|
||||
setMessageInput("");
|
||||
} catch (error) {
|
||||
const msg = String(error?.message || error);
|
||||
@@ -2822,6 +2895,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
setIsGeneratingKeys2(false);
|
||||
if (!shouldPreserveAnswerData()) {
|
||||
setShowAnswerStep(false);
|
||||
}
|
||||
@@ -2851,53 +2925,59 @@ var EnhancedSecureP2PChat = () => {
|
||||
document.dispatchEvent(new CustomEvent("peer-disconnect"));
|
||||
};
|
||||
const handleDisconnect = () => {
|
||||
setSessionTimeLeft(0);
|
||||
updateConnectionState({
|
||||
status: "disconnected",
|
||||
isUserInitiatedDisconnect: true
|
||||
});
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
}
|
||||
setKeyFingerprint("");
|
||||
setVerificationCode("");
|
||||
setSecurityLevel(null);
|
||||
setIsVerified(false);
|
||||
setShowVerification(false);
|
||||
setConnectionStatus("disconnected");
|
||||
setLocalVerificationConfirmed(false);
|
||||
setRemoteVerificationConfirmed(false);
|
||||
setBothVerificationsConfirmed(false);
|
||||
setConnectionStatus("disconnected");
|
||||
setShowVerification(false);
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
setShowAnswerStep(false);
|
||||
setKeyFingerprint("");
|
||||
setVerificationCode("");
|
||||
setSecurityLevel(null);
|
||||
setIsVerified(false);
|
||||
setMessages([]);
|
||||
if (typeof console.clear === "function") {
|
||||
console.clear();
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent("peer-disconnect"));
|
||||
document.dispatchEvent(new CustomEvent("disconnected"));
|
||||
document.dispatchEvent(new CustomEvent("session-cleanup", {
|
||||
detail: {
|
||||
timestamp: Date.now(),
|
||||
reason: "manual_disconnect"
|
||||
}
|
||||
}));
|
||||
setTimeout(() => {
|
||||
try {
|
||||
setSessionTimeLeft(0);
|
||||
}, 500);
|
||||
handleClearData();
|
||||
setTimeout(() => {
|
||||
}, 1e3);
|
||||
updateConnectionState({
|
||||
status: "disconnected",
|
||||
isUserInitiatedDisconnect: true
|
||||
});
|
||||
if (webrtcManagerRef2.current) {
|
||||
webrtcManagerRef2.current.disconnect();
|
||||
}
|
||||
if (notificationIntegrationRef.current) {
|
||||
notificationIntegrationRef.current.cleanup();
|
||||
notificationIntegrationRef.current = null;
|
||||
}
|
||||
setKeyFingerprint("");
|
||||
setVerificationCode("");
|
||||
setSecurityLevel(null);
|
||||
setIsVerified(false);
|
||||
setShowVerification(false);
|
||||
setConnectionStatus("disconnected");
|
||||
setLocalVerificationConfirmed(false);
|
||||
setRemoteVerificationConfirmed(false);
|
||||
setBothVerificationsConfirmed(false);
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
setOfferInput("");
|
||||
setAnswerInput("");
|
||||
setShowOfferStep(false);
|
||||
setShowAnswerStep(false);
|
||||
setIsGeneratingKeys2(false);
|
||||
setShowQRCode(false);
|
||||
setQrCodeUrl("");
|
||||
setShowQRScanner(false);
|
||||
setShowQRScannerModal(false);
|
||||
setMessages([]);
|
||||
if (typeof console.clear === "function") {
|
||||
console.clear();
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent("peer-disconnect"));
|
||||
document.dispatchEvent(new CustomEvent("disconnected"));
|
||||
document.dispatchEvent(new CustomEvent("session-cleanup", {
|
||||
detail: {
|
||||
timestamp: Date.now(),
|
||||
reason: "manual_disconnect"
|
||||
}
|
||||
}));
|
||||
handleClearData();
|
||||
setTimeout(() => {
|
||||
setSessionTimeLeft(0);
|
||||
}, 500);
|
||||
console.log("Disconnect completed successfully");
|
||||
} catch (error) {
|
||||
console.error("Error during disconnect:", error);
|
||||
}
|
||||
};
|
||||
const handleSessionActivated = (session) => {
|
||||
let message;
|
||||
@@ -3007,7 +3087,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
return React.createElement("div", {
|
||||
className: "minimal-bg min-h-screen"
|
||||
}, [
|
||||
React.createElement(EnhancedMinimalHeader, {
|
||||
window.EnhancedMinimalHeader && React.createElement(window.EnhancedMinimalHeader, {
|
||||
key: "header",
|
||||
status: connectionStatus,
|
||||
fingerprint: keyFingerprint,
|
||||
@@ -3016,8 +3096,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isConnected: isConnectedAndVerified,
|
||||
securityLevel,
|
||||
// sessionManager removed - all features enabled by default
|
||||
sessionTimeLeft,
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
webrtcManager: webrtcManagerRef2.current
|
||||
}),
|
||||
React.createElement(
|
||||
"main",
|
||||
@@ -3037,7 +3116,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
isVerified,
|
||||
chatMessagesRef,
|
||||
scrollToBottom,
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
webrtcManager: webrtcManagerRef2.current
|
||||
});
|
||||
})() : React.createElement(EnhancedConnectionSetup, {
|
||||
onCreateOffer: handleCreateOffer,
|
||||
@@ -3074,7 +3153,10 @@ var EnhancedSecureP2PChat = () => {
|
||||
nextQrFrame,
|
||||
prevQrFrame,
|
||||
// PAKE passwords removed - using SAS verification instead
|
||||
markAnswerCreated
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef,
|
||||
isGeneratingKeys,
|
||||
handleCreateOffer
|
||||
})
|
||||
),
|
||||
// QR Scanner Modal
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/app.jsx"],
|
||||
"sourcesContent": ["\r\n // Enhanced Copy Button with better UX\r\n const EnhancedCopyButton = ({ text, className = \"\", children }) => {\r\n const [copied, setCopied] = React.useState(false);\r\n \r\n const handleCopy = async () => {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n } catch (error) {\r\n console.error('Copy failed:', error);\r\n // Fallback for older browsers\r\n const textArea = document.createElement('textarea');\r\n textArea.value = text;\r\n document.body.appendChild(textArea);\r\n textArea.select();\r\n document.execCommand('copy');\r\n document.body.removeChild(textArea);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n }\r\n };\r\n \r\n return React.createElement('button', {\r\n onClick: handleCopy,\r\n className: `${className} transition-all duration-200`\r\n }, [\r\n React.createElement('i', {\r\n key: 'icon',\r\n className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`\r\n }),\r\n copied ? 'Copied!' : children\r\n ]);\r\n };\r\n \r\n // Verification Component\r\n const VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed, remoteConfirmed, bothConfirmed }) => {\r\n return React.createElement('div', {\r\n className: \"card-minimal rounded-xl p-6 border-purple-500/20\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'header',\r\n className: \"flex items-center mb-4\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'icon',\r\n className: \"w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3\"\r\n }, [\r\n React.createElement('i', {\r\n className: 'fas fa-shield-alt accent-purple'\r\n })\r\n ]),\r\n React.createElement('h3', {\r\n key: 'title',\r\n className: \"text-lg font-medium text-primary\"\r\n }, \"Security verification\")\r\n ]),\r\n React.createElement('div', {\r\n key: 'content',\r\n className: \"space-y-4\"\r\n }, [\r\n React.createElement('p', {\r\n key: 'description',\r\n className: \"text-secondary text-sm\"\r\n }, \"Verify the security code with your contact via another communication channel (voice, SMS, etc.):\"),\r\n React.createElement('div', {\r\n key: 'code-display',\r\n className: \"text-center\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'code',\r\n className: \"verification-code text-2xl py-4\"\r\n }, verificationCode)\r\n ]),\r\n // Verification status indicators\r\n React.createElement('div', {\r\n key: 'verification-status',\r\n className: \"space-y-2\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'local-status',\r\n className: `flex items-center justify-between p-2 rounded-lg ${localConfirmed ? 'bg-green-500/10 border border-green-500/20' : 'bg-gray-500/10 border border-gray-500/20'}`\r\n }, [\r\n React.createElement('span', {\r\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["e", "offerData", "payload"]
|
||||
"sourcesContent": ["\r\n // Enhanced Copy Button with better UX\r\n const EnhancedCopyButton = ({ text, className = \"\", children }) => {\r\n const [copied, setCopied] = React.useState(false);\r\n \r\n const handleCopy = async () => {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n } catch (error) {\r\n console.error('Copy failed:', error);\r\n // Fallback for older browsers\r\n const textArea = document.createElement('textarea');\r\n textArea.value = text;\r\n document.body.appendChild(textArea);\r\n textArea.select();\r\n document.execCommand('copy');\r\n document.body.removeChild(textArea);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n }\r\n };\r\n \r\n return React.createElement('button', {\r\n onClick: handleCopy,\r\n className: `${className} transition-all duration-200`\r\n }, [\r\n React.createElement('i', {\r\n key: 'icon',\r\n className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`\r\n }),\r\n copied ? 'Copied!' : children\r\n ]);\r\n };\r\n \r\n // Verification Component\r\n const VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed, remoteConfirmed, bothConfirmed }) => {\r\n return React.createElement('div', {\r\n className: \"card-minimal rounded-xl p-6 border-purple-500/20\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'header',\r\n className: \"flex items-center mb-4\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'icon',\r\n className: \"w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3\"\r\n }, [\r\n React.createElement('i', {\r\n className: 'fas fa-shield-alt accent-purple'\r\n })\r\n ]),\r\n React.createElement('h3', {\r\n key: 'title',\r\n className: \"text-lg font-medium text-primary\"\r\n }, \"Security verification\")\r\n ]),\r\n React.createElement('div', {\r\n key: 'content',\r\n className: \"space-y-4\"\r\n }, [\r\n React.createElement('p', {\r\n key: 'description',\r\n className: \"text-secondary text-sm\"\r\n }, \"Verify the security code with your contact via another communication channel (voice, SMS, etc.):\"),\r\n React.createElement('div', {\r\n key: 'code-display',\r\n className: \"text-center\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'code',\r\n className: \"verification-code text-2xl py-4\"\r\n }, verificationCode)\r\n ]),\r\n // Verification status indicators\r\n React.createElement('div', {\r\n key: 'verification-status',\r\n className: \"space-y-2\"\r\n }, [\r\n React.createElement('div', {\r\n key: 'local-status',\r\n className: `flex items-center justify-between p-2 rounded-lg ${localConfirmed ? 'bg-green-500/10 border border-green-500/20' : 'bg-gray-500/10 border border-gray-500/20'}`\r\n }, [\r\n React.createElement('span', {\r\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["setIsGeneratingKeys", "webrtcManagerRef", "offerData", "payload"]
|
||||
}
|
||||
@@ -1143,9 +1143,9 @@ await mutexManager._withMutex('connectionOperation', async () => {
|
||||
|
||||
## 🔗 Key Derivation
|
||||
|
||||
### HKDF Implementation
|
||||
### HKDF Implementation (RFC 5869 Compliant)
|
||||
|
||||
#### **Enhanced Key Derivation**
|
||||
#### **Enhanced Key Derivation with Proper Separation**
|
||||
```javascript
|
||||
async function deriveSharedKeys(privateKey, publicKey, salt) {
|
||||
// Validate inputs
|
||||
@@ -1159,43 +1159,132 @@ async function deriveSharedKeys(privateKey, publicKey, salt) {
|
||||
const saltBytes = new Uint8Array(salt);
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// Enhanced context info
|
||||
const contextInfo = encoder.encode('SecureBit.chat v4.0 Enhanced Security Edition');
|
||||
|
||||
// Derive master shared secret
|
||||
const sharedSecret = await crypto.subtle.deriveKey(
|
||||
// Step 1: Derive raw ECDH shared secret using pure ECDH
|
||||
const rawKeyMaterial = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'ECDH',
|
||||
public: publicKey
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: saltBytes,
|
||||
info: contextInfo
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
true, // Extractable for HKDF processing
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Export the raw key material
|
||||
const rawKeyData = await crypto.subtle.exportKey('raw', rawKeyMaterial);
|
||||
|
||||
// Import as HKDF key material for further derivation
|
||||
const rawSharedSecret = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
rawKeyData,
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
// Derive specific keys
|
||||
const keys = await Promise.all([
|
||||
deriveSpecificKey(sharedSecret, saltBytes, 'message-encryption-v4', 'AES-GCM', 256),
|
||||
deriveSpecificKey(sharedSecret, saltBytes, 'message-authentication-v4', 'HMAC', 384),
|
||||
deriveSpecificKey(sharedSecret, saltBytes, 'metadata-protection-v4', 'AES-GCM', 256),
|
||||
deriveSpecificKey(sharedSecret, saltBytes, 'fingerprint-generation-v4', 'AES-GCM', 256, true)
|
||||
]);
|
||||
// Step 2: Derive specific keys using HKDF with unique info parameters
|
||||
// Each key uses unique info parameter for proper separation
|
||||
|
||||
const [encryptionKey, macKey, metadataKey, fingerprintKey] = keys;
|
||||
// Derive message encryption key (messageKey)
|
||||
const messageKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-encryption-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable for enhanced security
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Generate key fingerprint
|
||||
// Derive MAC key for message authentication
|
||||
const macKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-authentication-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
false, // Non-extractable
|
||||
['sign', 'verify']
|
||||
);
|
||||
|
||||
// Derive Perfect Forward Secrecy key (pfsKey)
|
||||
const pfsKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('perfect-forward-secrecy-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Derive separate metadata encryption key
|
||||
const metadataKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('metadata-protection-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Generate temporary extractable key for fingerprint calculation
|
||||
const fingerprintKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('fingerprint-generation-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
true, // Extractable only for fingerprint
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Generate key fingerprint for verification
|
||||
const fingerprintKeyData = await crypto.subtle.exportKey('raw', fingerprintKey);
|
||||
const fingerprint = await generateKeyFingerprint(Array.from(new Uint8Array(fingerprintKeyData)));
|
||||
|
||||
return {
|
||||
encryptionKey,
|
||||
messageKey,
|
||||
macKey,
|
||||
pfsKey,
|
||||
metadataKey,
|
||||
fingerprint,
|
||||
timestamp: Date.now(),
|
||||
@@ -1204,31 +1293,13 @@ async function deriveSharedKeys(privateKey, publicKey, salt) {
|
||||
}
|
||||
```
|
||||
|
||||
#### **Specific Key Derivation**
|
||||
```javascript
|
||||
async function deriveSpecificKey(masterKey, salt, info, algorithm, keySize, extractable = false) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: salt,
|
||||
info: encoder.encode(info)
|
||||
},
|
||||
masterKey,
|
||||
algorithm === 'AES-GCM' ?
|
||||
{ name: 'AES-GCM', length: keySize } :
|
||||
{ name: 'HMAC', hash: `SHA-${keySize}` },
|
||||
extractable,
|
||||
algorithm === 'AES-GCM' ?
|
||||
['encrypt', 'decrypt'] :
|
||||
['sign', 'verify']
|
||||
);
|
||||
|
||||
return derivedKey;
|
||||
}
|
||||
```
|
||||
#### **HKDF Security Properties**
|
||||
- **RFC 5869 Compliance:** Full adherence to HMAC-based Extract-and-Expand Key Derivation Function standard
|
||||
- **Proper Key Separation:** Each derived key uses unique `info` parameter to prevent key reuse
|
||||
- **Salt Security:** 64-byte cryptographically secure salt for each derivation
|
||||
- **Non-Extractable Keys:** All operational keys are hardware-protected and non-exportable
|
||||
- **Forward Secrecy:** Independent key derivation for each session prevents key compromise propagation
|
||||
- **Algorithm Consistency:** SHA-256 hash function for optimal compatibility and performance
|
||||
|
||||
### Key Fingerprinting
|
||||
|
||||
|
||||
@@ -27,12 +27,13 @@ SecureBit.chat implements a revolutionary **18-layer security architecture** wit
|
||||
|
||||
## 🏗️ Security Architecture Overview
|
||||
|
||||
### 18-Layer Defense System
|
||||
### 19-Layer Defense System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ APPLICATION LAYER │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Layer 19: HKDF Key Derivation (RFC 5869 Compliant) │
|
||||
│ Layer 18: EC Point Validation (Format & Structure) │
|
||||
│ Layer 17: OID Validation (Algorithm & Curve Verification) │
|
||||
│ Layer 16: ASN.1 Validation (Complete Key Structure) │
|
||||
@@ -75,7 +76,7 @@ SecureBit.chat implements a revolutionary **18-layer security architecture** wit
|
||||
| 3 | 1-9 | High | + Timing attacks |
|
||||
| 4 | 1-12 | High Enhanced | + Advanced persistent threats |
|
||||
| 5 | 1-15 | Military-Grade | + Race conditions, Key exposure |
|
||||
| 6 | 1-18 | Maximum | + DTLS race conditions, Memory safety, Key structure validation |
|
||||
| 6 | 1-19 | Maximum | + DTLS race conditions, Memory safety, Key structure validation, HKDF compliance |
|
||||
|
||||
---
|
||||
|
||||
@@ -166,6 +167,55 @@ const validateOID = (parsed) => {
|
||||
};
|
||||
```
|
||||
|
||||
### Layer 19: HKDF Key Derivation (RFC 5869 Compliant)
|
||||
**Purpose:** RFC 5869 compliant key derivation with proper key separation and cryptographic security
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Standard:** RFC 5869 HMAC-based Extract-and-Expand Key Derivation Function
|
||||
- **Hash Function:** SHA-256 for optimal compatibility and performance
|
||||
- **Salt Security:** 64-byte cryptographically secure salt for each derivation
|
||||
- **Key Separation:** Unique `info` parameters for each derived key type
|
||||
- **Non-Extractable Keys:** Hardware-protected keys for enhanced security
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
// HKDF key derivation with proper separation
|
||||
const deriveSharedKeys = async (privateKey, publicKey, salt) => {
|
||||
// Step 1: Pure ECDH derivation
|
||||
const rawKeyMaterial = await crypto.subtle.deriveKey(
|
||||
{ name: 'ECDH', public: publicKey },
|
||||
privateKey,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true, // Extractable for HKDF processing
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Export and import for HKDF
|
||||
const rawKeyData = await crypto.subtle.exportKey('raw', rawKeyMaterial);
|
||||
const rawSharedSecret = await crypto.subtle.importKey(
|
||||
'raw', rawKeyData,
|
||||
{ name: 'HKDF', hash: 'SHA-256' },
|
||||
false, ['deriveKey']
|
||||
);
|
||||
|
||||
// Step 2: Derive specific keys with unique info parameters
|
||||
const messageKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-encryption-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false, ['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Additional keys derived with unique info parameters...
|
||||
return { messageKey, macKey, pfsKey, metadataKey, fingerprint };
|
||||
};
|
||||
```
|
||||
|
||||
### Layer 18: EC Point Validation (Format & Structure Verification)
|
||||
**Purpose:** Verification of elliptic curve point format and structure
|
||||
|
||||
@@ -403,12 +453,13 @@ webrtcManager.checkFakeTrafficStatus()
|
||||
|
||||
| Metric | Target | Current | Status |
|
||||
|--------|---------|---------|---------|
|
||||
| Active Security Layers | 12 | 12 | ✅ |
|
||||
| Active Security Layers | 19 | 19 | ✅ |
|
||||
| Encryption Strength | 256-bit | 256-bit | ✅ |
|
||||
| Key Exchange Security | P-384 | P-384 | ✅ |
|
||||
| Forward Secrecy | Complete | Complete | ✅ |
|
||||
| Traffic Obfuscation | Maximum | Maximum | ✅ |
|
||||
| Attack Surface | Minimal | Minimal | ✅ |
|
||||
| HKDF Compliance | RFC 5869 | RFC 5869 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self' ;
|
||||
script-src 'self';
|
||||
style-src 'self';
|
||||
font-src 'self' ;
|
||||
connect-src 'self' https: ;
|
||||
img-src 'self' data:;
|
||||
manifest-src 'self';
|
||||
worker-src 'self';
|
||||
font-src 'self';
|
||||
connect-src 'self' https: wss: ws:;
|
||||
img-src 'self' data: https:;
|
||||
media-src 'none';
|
||||
object-src 'none';
|
||||
frame-src 'none';
|
||||
frame-ancestors 'none';
|
||||
worker-src 'self';
|
||||
manifest-src 'self';
|
||||
form-action 'self';
|
||||
upgrade-insecure-requests;
|
||||
report-uri /csp-report;
|
||||
@@ -36,15 +38,58 @@
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
<meta name="msapplication-config" content="/browserconfig.xml">
|
||||
|
||||
<!-- iOS Splash Screens -->
|
||||
<link rel="apple-touch-startup-image" href="./logo/splash/launch-640x1136.png"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="./logo/splash/launch-750x1334.png"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="./logo/splash/launch-1125x2436.png"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<link rel="apple-touch-startup-image" href="./logo/splash/launch-1242x2688.png"
|
||||
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
|
||||
<!-- iOS Splash Screens - Полная конфигурация с генератора -->
|
||||
<!-- Landscape -->
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_17_Pro_Max__iPhone_16_Pro_Max_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_17_Pro__iPhone_17__iPhone_16_Pro_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 420px) and (device-height: 912px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_Air_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_16e__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_11__iPhone_XR_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_landscape.png">
|
||||
|
||||
<!-- iPad Landscape -->
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/13__iPad_Pro_M4_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/12.9__iPad_Pro_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/11__iPad_Pro_M4_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/11__iPad_Pro__10.5__iPad_Pro_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/10.9__iPad_Air_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/10.5__iPad_Air_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/10.2__iPad_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_landscape.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="./logo/splash/splash_screens/8.3__iPad_Mini_landscape.png">
|
||||
|
||||
<!-- Portrait -->
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_17_Pro_Max__iPhone_16_Pro_Max_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_17_Pro__iPhone_17__iPhone_16_Pro_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_16_Plus__iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 420px) and (device-height: 912px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_Air_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_16__iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_16e__iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_11__iPhone_XR_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/4__iPhone_SE__iPod_touch_5th_generation_and_later_portrait.png">
|
||||
|
||||
<!-- iPad Portrait -->
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 1032px) and (device-height: 1376px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/13__iPad_Pro_M4_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/12.9__iPad_Pro_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1210px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/11__iPad_Pro_M4_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/10.9__iPad_Air_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/10.5__iPad_Air_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/10.2__iPad_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png">
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png">
|
||||
@@ -67,12 +112,11 @@
|
||||
<meta name="theme-color" content="#ff6b35">
|
||||
<meta name="msapplication-navbutton-color" content="#ff6b35">
|
||||
|
||||
<!-- Security Headers for PWA -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' ; style-src 'self' ; font-src 'self' ; connect-src 'self' https: wss: ws:; img-src 'self' data: https:; media-src 'none'; object-src 'none'; frame-src 'none'; worker-src 'self';">
|
||||
<!-- Security Headers for PWA - CSP is already defined above -->
|
||||
|
||||
|
||||
<!-- GitHub Pages SEO -->
|
||||
<meta name="description" content="SecureBit.chat v4.3.120 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
|
||||
<meta name="description" content="SecureBit.chat v4.4.18 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
|
||||
<meta name="keywords" content="P2P messenger, ECDH, DTLS, SAS, encryption, WebRTC, privacy, ASN.1 validation, military-grade security, 18-layer defense, MITM protection, PFS">
|
||||
<meta name="author" content="Volodymyr">
|
||||
<link rel="canonical" href="https://github.com/SecureBitChat/securebit-chat/">
|
||||
@@ -108,12 +152,13 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app.js?v=1757383304"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1757383304"></script>
|
||||
<script type="module" src="dist/app.js?v=1757383304"></script>
|
||||
|
||||
<script src="src/scripts/pwa-register.js"></script>
|
||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js" type="module"></script>
|
||||
<script src="./src/scripts/pwa-offline-test.js"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css">
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "SecureBit.chat v4.3.120 - ECDH + DTLS + SAS",
|
||||
"name": "SecureBit.chat v4.7.53 - ECDH + DTLS + SAS",
|
||||
"short_name": "SecureBit",
|
||||
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
|
||||
"start_url": "./",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'message',
|
||||
className: "text-sm"
|
||||
className: "text-sm break-words whitespace-normal"
|
||||
}, message),
|
||||
timestamp && React.createElement('div', {
|
||||
key: 'meta',
|
||||
@@ -280,12 +280,17 @@
|
||||
toggleQrManualMode,
|
||||
nextQrFrame,
|
||||
prevQrFrame,
|
||||
markAnswerCreated
|
||||
markAnswerCreated,
|
||||
notificationIntegrationRef,
|
||||
isGeneratingKeys,
|
||||
handleCreateOffer
|
||||
}) => {
|
||||
const [mode, setMode] = React.useState('select');
|
||||
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
|
||||
|
||||
const resetToSelect = () => {
|
||||
setMode('select');
|
||||
setIsGeneratingKeys(false);
|
||||
onClearData();
|
||||
};
|
||||
|
||||
@@ -296,6 +301,112 @@
|
||||
const handleVerificationReject = () => {
|
||||
onVerifyConnection(false);
|
||||
};
|
||||
|
||||
// Request notification permission on first user interaction
|
||||
const requestNotificationPermissionOnInteraction = async () => {
|
||||
if (notificationPermissionRequested) {
|
||||
return; // Already requested
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if Notification API is supported
|
||||
if (!('Notification' in window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in a secure context
|
||||
if (!window.isSecureContext && window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current permission status
|
||||
const currentPermission = (typeof Notification !== 'undefined' && Notification)
|
||||
? Notification.permission
|
||||
: 'denied';
|
||||
|
||||
// Only request if permission is default (not granted or denied)
|
||||
if (currentPermission === 'default' && typeof Notification !== 'undefined' && Notification) {
|
||||
const permission = await Notification.requestPermission();
|
||||
|
||||
if (permission === 'granted') {
|
||||
// Initialize notification integration immediately
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
|
||||
// Store reference for cleanup
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
|
||||
// Send welcome notification
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const welcomeNotification = new Notification('SecureBit Chat', {
|
||||
body: 'Notifications enabled! You will receive alerts for new messages.',
|
||||
icon: '/logo/icon-192x192.png',
|
||||
tag: 'welcome-notification'
|
||||
});
|
||||
|
||||
welcomeNotification.onclick = () => {
|
||||
welcomeNotification.close();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
welcomeNotification.close();
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
}
|
||||
} else if (currentPermission === 'granted') {
|
||||
// Initialize notification integration immediately
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
|
||||
// Store reference for cleanup
|
||||
notificationIntegrationRef.current = integration;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
|
||||
// Test notification to confirm it works
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const testNotification = new Notification('SecureBit Chat', {
|
||||
body: 'Notifications are working! You will receive alerts for new messages.',
|
||||
icon: '/logo/icon-192x192.png',
|
||||
tag: 'test-notification'
|
||||
});
|
||||
|
||||
testNotification.onclick = () => {
|
||||
testNotification.close();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
testNotification.close();
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setNotificationPermissionRequested(true);
|
||||
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
};
|
||||
|
||||
if (showVerification) {
|
||||
return React.createElement('div', {
|
||||
@@ -346,7 +457,16 @@
|
||||
// Create Connection
|
||||
React.createElement('div', {
|
||||
key: 'create',
|
||||
onClick: () => setMode('create'),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode('create');
|
||||
// Автоматически запускаем генерацию ключей
|
||||
setTimeout(() => {
|
||||
if (webrtcManagerRef.current) {
|
||||
handleCreateOffer();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
@@ -418,7 +538,10 @@
|
||||
// Join Connection
|
||||
React.createElement('div', {
|
||||
key: 'join',
|
||||
onClick: () => setMode('join'),
|
||||
onClick: () => {
|
||||
requestNotificationPermissionOnInteraction();
|
||||
setMode('join');
|
||||
},
|
||||
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
@@ -515,7 +638,7 @@
|
||||
className: "text-xl font-semibold text-primary mb-2"
|
||||
}, 'Creating a secure channel')
|
||||
]),
|
||||
|
||||
|
||||
// Step 1
|
||||
!showAnswerStep && React.createElement('div', {
|
||||
key: 'step1',
|
||||
@@ -538,16 +661,15 @@
|
||||
key: 'description',
|
||||
className: "text-secondary text-sm mb-4"
|
||||
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
||||
!showOfferStep && React.createElement('button', {
|
||||
key: 'create-btn',
|
||||
onClick: onCreateOffer,
|
||||
disabled: connectionStatus === 'connecting',
|
||||
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
||||
!showOfferStep && isGeneratingKeys && React.createElement('div', {
|
||||
key: 'loading-state',
|
||||
className: "w-full py-3 px-4 rounded-lg font-medium transition-all duration-200 bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-shield-alt mr-2'
|
||||
key: 'spinner',
|
||||
className: 'fas fa-spinner fa-spin mr-2'
|
||||
}),
|
||||
'Create secure keys'
|
||||
'Generating secure keys...'
|
||||
]),
|
||||
|
||||
showOfferStep && React.createElement('div', {
|
||||
@@ -591,7 +713,7 @@
|
||||
} catch { return typeof offerData === 'object' ? JSON.stringify(offerData) : (offerData || ''); }
|
||||
})(),
|
||||
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
|
||||
}, 'Copy invitation code')
|
||||
}, 'Copy invitation code'),
|
||||
]),
|
||||
showQRCode && qrCodeUrl && React.createElement('div', {
|
||||
key: 'qr-container',
|
||||
@@ -653,63 +775,8 @@
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
]),
|
||||
|
||||
// Step 2 - Session Type Selection
|
||||
// showOfferStep && React.createElement('div', {
|
||||
// key: 'step2',
|
||||
// className: "card-minimal rounded-xl p-6"
|
||||
// }, [
|
||||
// React.createElement('div', {
|
||||
// key: 'step-header',
|
||||
// className: "flex items-center mb-4"
|
||||
// }, [
|
||||
// React.createElement('div', {
|
||||
// key: 'number',
|
||||
// className: "w-8 h-8 bg-green-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
||||
// }, '2'),
|
||||
// React.createElement('h3', {
|
||||
// key: 'title',
|
||||
// className: "text-lg font-medium text-primary"
|
||||
// }, "Select session type")
|
||||
// ]),
|
||||
// React.createElement('p', {
|
||||
// key: 'description',
|
||||
// className: "text-secondary text-sm mb-4"
|
||||
// }, "Choose a session plan or use limited demo mode for testing."),
|
||||
// React.createElement(SessionTypeSelector, {
|
||||
// key: 'session-selector',
|
||||
// onSelectType: (sessionType) => {
|
||||
// // Save the selected session type
|
||||
// setSelectedSessionType(sessionType);
|
||||
// console.log('🎯 Session type selected:', sessionType);
|
||||
|
||||
// // FIX: For demo sessions, we immediately call automatic activation
|
||||
// if (sessionType === 'demo') {
|
||||
// console.log('🎮 Demo session selected, scheduling automatic activation...');
|
||||
// // Delay activation for 2 seconds to stabilize
|
||||
// setTimeout(() => {
|
||||
// if (sessionManager) {
|
||||
// console.log('🚀 Triggering demo session activation from selection...');
|
||||
// handleDemoVerification();
|
||||
// }
|
||||
// }, 2000);
|
||||
// }
|
||||
|
||||
// // Open a modal payment window
|
||||
// if (typeof window.showPaymentModal === 'function') {
|
||||
// window.showPaymentModal(sessionType);
|
||||
// } else {
|
||||
// // Fallback - show session information
|
||||
// console.log('Selected session type:', sessionType);
|
||||
// }
|
||||
// },
|
||||
// onCancel: resetToSelect,
|
||||
// sessionManager: window.sessionManager
|
||||
// })
|
||||
// ]),
|
||||
|
||||
// Step 3 - Waiting for response
|
||||
showOfferStep && React.createElement('div', {
|
||||
key: 'step2',
|
||||
className: "card-minimal rounded-xl p-6"
|
||||
@@ -745,7 +812,7 @@
|
||||
className: 'fas fa-qrcode mr-2'
|
||||
}),
|
||||
'Scan QR Code'
|
||||
])
|
||||
]),
|
||||
]),
|
||||
React.createElement('textarea', {
|
||||
key: 'input',
|
||||
@@ -1078,6 +1145,7 @@
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const EnhancedChatInterface = ({
|
||||
messages,
|
||||
messageInput,
|
||||
@@ -1410,8 +1478,12 @@
|
||||
const [qrCodeUrl, setQrCodeUrl] = React.useState('');
|
||||
const [showQRScanner, setShowQRScanner] = React.useState(false);
|
||||
const [showQRScannerModal, setShowQRScannerModal] = React.useState(false);
|
||||
const [isGeneratingKeys, setIsGeneratingKeys] = React.useState(false);
|
||||
|
||||
|
||||
const [isVerified, setIsVerified] = React.useState(false);
|
||||
const [securityLevel, setSecurityLevel] = React.useState(null);
|
||||
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
||||
|
||||
// Mutual verification states
|
||||
const [localVerificationConfirmed, setLocalVerificationConfirmed] = React.useState(false);
|
||||
@@ -1421,7 +1493,6 @@
|
||||
// PAKE password states removed - using SAS verification instead
|
||||
|
||||
// Session state - all security features enabled by default
|
||||
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
||||
const [pendingSession, setPendingSession] = React.useState(null);
|
||||
|
||||
// All security features are enabled by default - no payment required
|
||||
@@ -1459,7 +1530,7 @@
|
||||
const maxPreserveTime = 300000;
|
||||
|
||||
|
||||
const hasAnswerData = (answerData && answerData.trim().length > 0) ||
|
||||
const hasAnswerData = (answerData && typeof answerData === 'string' && answerData.trim().length > 0) ||
|
||||
(answerInput && answerInput.trim().length > 0);
|
||||
|
||||
const hasAnswerQR = qrCodeUrl && qrCodeUrl.trim().length > 0;
|
||||
@@ -1506,6 +1577,7 @@
|
||||
}, []);
|
||||
|
||||
const webrtcManagerRef = React.useRef(null);
|
||||
const notificationIntegrationRef = React.useRef(null);
|
||||
// Expose for modules/UI that run outside this closure (e.g., inline handlers)
|
||||
// Safe because it's a ref object and we maintain it centrally here
|
||||
window.webrtcManagerRef = webrtcManagerRef;
|
||||
@@ -1597,14 +1669,7 @@
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Session time ticker - unlimited sessions
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
// Sessions are unlimited - no time restrictions
|
||||
setSessionTimeLeft(0);
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
// Session time ticker removed - sessions are unlimited
|
||||
|
||||
// Sessions are unlimited - no expiration handler needed
|
||||
|
||||
@@ -1720,8 +1785,8 @@
|
||||
setBothVerificationsConfirmed(false);
|
||||
|
||||
// Clear connection data
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData('');
|
||||
setAnswerData('');
|
||||
setOfferInput('');
|
||||
setAnswerInput('');
|
||||
setShowOfferStep(false);
|
||||
@@ -1730,16 +1795,14 @@
|
||||
setVerificationCode('');
|
||||
setSecurityLevel(null);
|
||||
|
||||
// Reset session and timer
|
||||
setSessionTimeLeft(0);
|
||||
|
||||
// Return to main page after a short delay
|
||||
setTimeout(() => {
|
||||
setConnectionStatus('disconnected');
|
||||
setShowVerification(false);
|
||||
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData('');
|
||||
setAnswerData('');
|
||||
setOfferInput('');
|
||||
setAnswerInput('');
|
||||
setShowOfferStep(false);
|
||||
@@ -1767,8 +1830,8 @@
|
||||
setBothVerificationsConfirmed(false);
|
||||
|
||||
// Clear connection data
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData('');
|
||||
setAnswerData('');
|
||||
setOfferInput('');
|
||||
setAnswerInput('');
|
||||
setShowOfferStep(false);
|
||||
@@ -1847,7 +1910,21 @@
|
||||
handleVerificationStateChange
|
||||
);
|
||||
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.3.120 - 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');
|
||||
// Initialize notification integration if permission was already granted
|
||||
if (typeof Notification !== 'undefined' && Notification && Notification.permission === 'granted' && window.NotificationIntegration && !notificationIntegrationRef.current) {
|
||||
try {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
integration.init().then(() => {
|
||||
notificationIntegrationRef.current = integration;
|
||||
}).catch((error) => {
|
||||
// Handle error silently
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.53 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
|
||||
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (event.type === 'beforeunload' && !isTabSwitching) {
|
||||
@@ -2512,7 +2589,7 @@
|
||||
}
|
||||
|
||||
// Single QR code - try to decode directly
|
||||
console.log('Processing single QR code:', scannedData.substring(0, 50) + '...');
|
||||
// Removed verbose debug log
|
||||
let parsedData;
|
||||
if (typeof window.decodeAnyPayload === 'function') {
|
||||
const any = window.decodeAnyPayload(scannedData);
|
||||
@@ -2723,11 +2800,14 @@
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const handleCreateOffer = async () => {
|
||||
try {
|
||||
// All security features are enabled by default
|
||||
|
||||
setIsGeneratingKeys(true);
|
||||
setOfferData('');
|
||||
setShowOfferStep(false);
|
||||
setShowQRCode(false);
|
||||
@@ -2746,10 +2826,19 @@
|
||||
const bin = window.encodeBinaryToPrefixed(offerString);
|
||||
// Force chunking into 4 parts - split binary data directly
|
||||
const TARGET_CHUNKS = 4;
|
||||
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||
let total = TARGET_CHUNKS;
|
||||
let FRAME_MAX = Math.max(200, Math.ceil(bin.length / TARGET_CHUNKS));
|
||||
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||
if (total < 2) { total = 2; FRAME_MAX = Math.ceil(bin.length / 2) || 1; }
|
||||
|
||||
// Ensure we don't exceed TARGET_CHUNKS
|
||||
if (bin.length <= FRAME_MAX) {
|
||||
total = 1;
|
||||
FRAME_MAX = bin.length;
|
||||
} else {
|
||||
// Recalculate to ensure exactly TARGET_CHUNKS parts
|
||||
FRAME_MAX = Math.ceil(bin.length / TARGET_CHUNKS);
|
||||
total = TARGET_CHUNKS;
|
||||
}
|
||||
|
||||
const id = `bin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const chunks = [];
|
||||
@@ -2822,6 +2911,8 @@
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} finally {
|
||||
setIsGeneratingKeys(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2883,10 +2974,19 @@
|
||||
const bin = window.encodeBinaryToPrefixed(answerString);
|
||||
// Force chunking into 4 parts - split binary data directly
|
||||
const TARGET_CHUNKS = 4;
|
||||
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||
let total = TARGET_CHUNKS;
|
||||
let FRAME_MAX = Math.max(200, Math.ceil(bin.length / TARGET_CHUNKS));
|
||||
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||
if (total < 2) { total = 2; FRAME_MAX = Math.ceil(bin.length / 2) || 1; }
|
||||
|
||||
// Ensure we don't exceed TARGET_CHUNKS
|
||||
if (bin.length <= FRAME_MAX) {
|
||||
total = 1;
|
||||
FRAME_MAX = bin.length;
|
||||
} else {
|
||||
// Recalculate to ensure exactly TARGET_CHUNKS parts
|
||||
FRAME_MAX = Math.ceil(bin.length / TARGET_CHUNKS);
|
||||
total = TARGET_CHUNKS;
|
||||
}
|
||||
|
||||
const id = `ans_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const chunks = [];
|
||||
@@ -2925,7 +3025,7 @@
|
||||
}
|
||||
|
||||
// Mark answer as created for state management
|
||||
if (e.target.value.trim().length > 0) {
|
||||
if (answerInput.trim().length > 0) {
|
||||
if (typeof markAnswerCreated === 'function') {
|
||||
markAnswerCreated();
|
||||
}
|
||||
@@ -3124,11 +3224,47 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyConnection = (isValid) => {
|
||||
const handleVerifyConnection = async (isValid) => {
|
||||
if (isValid) {
|
||||
webrtcManagerRef.current.confirmVerification();
|
||||
// Mark local verification as confirmed
|
||||
setLocalVerificationConfirmed(true);
|
||||
|
||||
// Initialize notification integration if permission was granted
|
||||
try {
|
||||
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
|
||||
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
|
||||
await integration.init();
|
||||
|
||||
// Store reference for cleanup
|
||||
notificationIntegrationRef.current = integration;
|
||||
|
||||
|
||||
// Check if permission was already granted
|
||||
const status = integration.getStatus();
|
||||
if (status.permission === 'granted') {
|
||||
setMessages(prev => [...prev, {
|
||||
message: '✓ Notifications enabled - you will receive alerts when the tab is inactive',
|
||||
type: 'system',
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
} else {
|
||||
setMessages(prev => [...prev, {
|
||||
message: 'ℹ Notifications disabled - you can enable them using the button on the main page',
|
||||
type: 'system',
|
||||
id: Date.now(),
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
}
|
||||
} else if (notificationIntegrationRef.current) {
|
||||
} else {
|
||||
// Handle error silently
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize notifications:', error);
|
||||
// Don't show error to user, notifications are optional
|
||||
}
|
||||
} else {
|
||||
setMessages(prev => [...prev, {
|
||||
message: ' Verification rejected. The connection is unsafe! Session reset..',
|
||||
@@ -3146,8 +3282,8 @@
|
||||
|
||||
// Reset UI to initial state
|
||||
setConnectionStatus('disconnected');
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
setOfferData('');
|
||||
setAnswerData('');
|
||||
setOfferInput('');
|
||||
setAnswerInput('');
|
||||
setShowOfferStep(false);
|
||||
@@ -3202,6 +3338,7 @@
|
||||
setOfferInput('');
|
||||
setAnswerInput('');
|
||||
setShowOfferStep(false);
|
||||
setIsGeneratingKeys(false);
|
||||
|
||||
if (!shouldPreserveAnswerData()) {
|
||||
setShowAnswerStep(false);
|
||||
@@ -3246,6 +3383,7 @@
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
try {
|
||||
setSessionTimeLeft(0);
|
||||
|
||||
// Mark as user-initiated disconnect
|
||||
@@ -3254,11 +3392,18 @@
|
||||
isUserInitiatedDisconnect: true
|
||||
});
|
||||
|
||||
// Cleanup session state
|
||||
// Cleanup WebRTC connection
|
||||
if (webrtcManagerRef.current) {
|
||||
webrtcManagerRef.current.disconnect();
|
||||
}
|
||||
|
||||
// Cleanup notification integration
|
||||
if (notificationIntegrationRef.current) {
|
||||
notificationIntegrationRef.current.cleanup();
|
||||
notificationIntegrationRef.current = null;
|
||||
}
|
||||
|
||||
// Clear all connection-related states
|
||||
setKeyFingerprint('');
|
||||
setVerificationCode('');
|
||||
setSecurityLevel(null);
|
||||
@@ -3271,45 +3416,50 @@
|
||||
setRemoteVerificationConfirmed(false);
|
||||
setBothVerificationsConfirmed(false);
|
||||
|
||||
// Reset UI to initial state (user-initiated disconnect always clears data)
|
||||
setConnectionStatus('disconnected');
|
||||
setShowVerification(false);
|
||||
setOfferData(null);
|
||||
setAnswerData(null);
|
||||
// Reset UI to initial state
|
||||
setOfferData('');
|
||||
setAnswerData('');
|
||||
setOfferInput('');
|
||||
setAnswerInput('');
|
||||
setShowOfferStep(false);
|
||||
setShowAnswerStep(false);
|
||||
setKeyFingerprint('');
|
||||
setVerificationCode('');
|
||||
setSecurityLevel(null);
|
||||
setIsVerified(false);
|
||||
|
||||
setIsGeneratingKeys(false);
|
||||
setShowQRCode(false);
|
||||
setQrCodeUrl('');
|
||||
setShowQRScanner(false);
|
||||
setShowQRScannerModal(false);
|
||||
|
||||
// Clear messages
|
||||
setMessages([]);
|
||||
|
||||
// Clear console
|
||||
if (typeof console.clear === 'function') {
|
||||
console.clear();
|
||||
}
|
||||
|
||||
// Dispatch disconnect events
|
||||
document.dispatchEvent(new CustomEvent('peer-disconnect'));
|
||||
document.dispatchEvent(new CustomEvent('disconnected'));
|
||||
|
||||
// Dispatch session cleanup event
|
||||
document.dispatchEvent(new CustomEvent('session-cleanup', {
|
||||
detail: {
|
||||
timestamp: Date.now(),
|
||||
reason: 'manual_disconnect'
|
||||
}
|
||||
}));
|
||||
|
||||
// Clear data and reset session timer
|
||||
handleClearData();
|
||||
|
||||
setTimeout(() => {
|
||||
setSessionTimeLeft(0);
|
||||
}, 500);
|
||||
|
||||
handleClearData();
|
||||
|
||||
setTimeout(() => {
|
||||
// Session manager removed - all features enabled by default
|
||||
}, 1000);
|
||||
console.log('Disconnect completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error during disconnect:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSessionActivated = (session) => {
|
||||
@@ -3444,7 +3594,7 @@
|
||||
return React.createElement('div', {
|
||||
className: "minimal-bg min-h-screen"
|
||||
}, [
|
||||
React.createElement(EnhancedMinimalHeader, {
|
||||
window.EnhancedMinimalHeader && React.createElement(window.EnhancedMinimalHeader, {
|
||||
key: 'header',
|
||||
status: connectionStatus,
|
||||
fingerprint: keyFingerprint,
|
||||
@@ -3453,7 +3603,6 @@
|
||||
isConnected: isConnectedAndVerified,
|
||||
securityLevel: securityLevel,
|
||||
// sessionManager removed - all features enabled by default
|
||||
sessionTimeLeft: sessionTimeLeft,
|
||||
webrtcManager: webrtcManagerRef.current
|
||||
}),
|
||||
|
||||
@@ -3512,7 +3661,10 @@
|
||||
nextQrFrame: nextQrFrame,
|
||||
prevQrFrame: prevQrFrame,
|
||||
// PAKE passwords removed - using SAS verification instead
|
||||
markAnswerCreated: markAnswerCreated
|
||||
markAnswerCreated: markAnswerCreated,
|
||||
notificationIntegrationRef: notificationIntegrationRef,
|
||||
isGeneratingKeys: isGeneratingKeys,
|
||||
handleCreateOffer: handleCreateOffer
|
||||
})
|
||||
),
|
||||
|
||||
@@ -3590,7 +3742,8 @@
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
|
||||
]);
|
||||
};
|
||||
@@ -3621,6 +3774,7 @@
|
||||
if (!window.initializeApp) {
|
||||
window.initializeApp = initializeApp;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render Enhanced Application
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
||||
@@ -92,13 +92,6 @@
|
||||
session: { status: "check", detail: "Random session ID" },
|
||||
},
|
||||
{
|
||||
name: "Payment Integration",
|
||||
lockbit: { status: "trophy", detail: "Lightning Network satoshis per session + WebLN" },
|
||||
signal: { status: "times", detail: "No payment system" },
|
||||
threema: { status: "times", detail: "No payment system" },
|
||||
session: { status: "times", detail: "No payment system" },
|
||||
},
|
||||
{
|
||||
name: "Metadata Protection",
|
||||
lockbit: { status: "trophy", detail: "Full metadata encryption + traffic obfuscation" },
|
||||
signal: { status: "warning", detail: "Sealed Sender (partial)" },
|
||||
@@ -127,13 +120,6 @@
|
||||
session: { status: "warning", detail: "Basic key verification" },
|
||||
},
|
||||
{
|
||||
name: "Economic Model",
|
||||
lockbit: { status: "trophy", detail: "Sustainable pay-per-session model" },
|
||||
signal: { status: "warning", detail: "Donations and grants dependency" },
|
||||
threema: { status: "check", detail: "One-time app purchase" },
|
||||
session: { status: "warning", detail: "Donations dependency" },
|
||||
},
|
||||
{
|
||||
name: "Censorship Resistance",
|
||||
lockbit: { status: "trophy", detail: "Impossible to block P2P + no servers to target" },
|
||||
signal: { status: "warning", detail: "Blocked in authoritarian countries" },
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
const DownloadApps = () => {
|
||||
const apps = [
|
||||
{ id: 'web', name: 'Web App', subtitle: 'Browser Version', icon: 'fas fa-globe', platform: 'Web', isActive: true, url: 'https://securebit.chat/', color: 'green' },
|
||||
{ id: 'windows', name: 'Windows', subtitle: 'Desktop App', icon: 'fab fa-windows', platform: 'Desktop', isActive: true, url: 'https://securebit.chat/download/windows/SecureBit%20Chat%20Setup%204.1.222.exe', color: 'blue' },
|
||||
{ id: 'macos', name: 'macOS', subtitle: 'Desktop App', icon: 'fab fa-safari', platform: 'Desktop', isActive: false, url: '#', color: 'gray' },
|
||||
{ id: 'linux', name: 'Linux', subtitle: 'Desktop App', icon: 'fab fa-linux', platform: 'Desktop', isActive: false, url: '#', color: 'orange' },
|
||||
{ id: 'windows', name: 'Windows', subtitle: 'Desktop App', icon: 'fab fa-windows', platform: 'Desktop', isActive: true, url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe', color: 'blue' },
|
||||
{ id: 'macos', name: 'macOS', subtitle: 'Desktop App', icon: 'fab fa-safari', platform: 'Desktop', isActive: true, url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBitChat.zip', color: 'gray' },
|
||||
{ id: 'linux', name: 'Linux', subtitle: 'Desktop App', icon: 'fab fa-linux', platform: 'Desktop', isActive: true, url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage', color: 'orange' },
|
||||
{ id: 'ios', name: 'iOS', subtitle: 'iPhone & iPad', icon: 'fab fa-apple', platform: 'Mobile', isActive: false, url: 'https://apps.apple.com/app/securebit-chat/', color: 'white' },
|
||||
{ id: 'android', name: 'Android', subtitle: 'Google Play', icon: 'fab fa-android', platform: 'Mobile', isActive: false, url: 'https://play.google.com/store/apps/details?id=com.securebit.chat', color: 'green' }
|
||||
{ id: 'android', name: 'Android', subtitle: 'Google Play', icon: 'fab fa-android', platform: 'Mobile', isActive: false, url: 'https://play.google.com/store/apps/details?id=com.securebit.chat', color: 'green' },
|
||||
{ id: 'chrome', name: 'Chrome', subtitle: 'Browser Extension', icon: 'fab fa-chrome', platform: 'Browser', isActive: false, url: '#', color: 'yellow' },
|
||||
{ id: 'edge', name: 'Edge', subtitle: 'Browser Extension', icon: 'fab fa-edge', platform: 'Browser', isActive: false, url: '#', color: 'blue' },
|
||||
{ id: 'opera', name: 'Opera', subtitle: 'Browser Extension', icon: 'fab fa-opera', platform: 'Browser', isActive: false, url: '#', color: 'red' },
|
||||
{ id: 'firefox', name: 'Firefox', subtitle: 'Browser Extension', icon: 'fab fa-firefox-browser', platform: 'Browser', isActive: false, url: '#', color: 'orange' },
|
||||
];
|
||||
|
||||
const handleDownload = (app) => {
|
||||
if (app.isActive) window.open(app.url, '_blank');
|
||||
};
|
||||
|
||||
const desktopApps = apps.filter(a => a.platform !== 'Mobile');
|
||||
const desktopApps = apps.filter(a => a.platform === 'Desktop' || a.platform === 'Web');
|
||||
const mobileApps = apps.filter(a => a.platform === 'Mobile');
|
||||
const browserApps = apps.filter(a => a.platform === 'Browser');
|
||||
|
||||
const cardSize = "w-28 h-28";
|
||||
const cardSize = "w-28 h-28";
|
||||
|
||||
const colorClasses = {
|
||||
green: 'text-green-500',
|
||||
blue: 'text-blue-500',
|
||||
blueios: 'text-blue-600',
|
||||
gray: 'text-gray-500',
|
||||
orange: 'text-orange-500',
|
||||
red: 'text-red-500',
|
||||
white: 'text-white',
|
||||
yellow: 'text-yellow-400',
|
||||
};
|
||||
|
||||
const renderAppCard = (app) => (
|
||||
@@ -65,8 +72,13 @@ const DownloadApps = () => {
|
||||
),
|
||||
|
||||
// Mobile Apps
|
||||
React.createElement('div', { key: 'mobile-row', className: "flex justify-center gap-6" },
|
||||
React.createElement('div', { key: 'mobile-row', className: "flex justify-center gap-6 mb-6" },
|
||||
mobileApps.map(renderAppCard)
|
||||
),
|
||||
|
||||
// Browser Extensions
|
||||
React.createElement('div', { key: 'browser-row', className: "flex justify-center gap-6" },
|
||||
browserApps.map(renderAppCard)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -5,15 +5,14 @@ const EnhancedMinimalHeader = ({
|
||||
onDisconnect,
|
||||
isConnected,
|
||||
securityLevel,
|
||||
sessionManager,
|
||||
sessionTimeLeft,
|
||||
webrtcManager
|
||||
}) => {
|
||||
const [currentTimeLeft, setCurrentTimeLeft] = React.useState(sessionTimeLeft || 0);
|
||||
const [hasActiveSession, setHasActiveSession] = React.useState(false);
|
||||
const [sessionType, setSessionType] = React.useState('unknown');
|
||||
const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
|
||||
const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
|
||||
// Added local session state to remove references errors after session timer removal
|
||||
const [hasActiveSession, setHasActiveSession] = React.useState(false);
|
||||
const [currentTimeLeft, setCurrentTimeLeft] = React.useState(0);
|
||||
const [sessionType, setSessionType] = React.useState('unknown');
|
||||
|
||||
// ============================================
|
||||
// FIXED SECURITY UPDATE LOGIC
|
||||
@@ -64,11 +63,11 @@ const EnhancedMinimalHeader = ({
|
||||
} else if (window.DEBUG_MODE) {
|
||||
}
|
||||
} else {
|
||||
console.warn(' Security calculation returned invalid data');
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(' Error in real security calculation:', error);
|
||||
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
}
|
||||
@@ -126,11 +125,11 @@ const EnhancedMinimalHeader = ({
|
||||
if (securityData && securityData.isRealData !== false) {
|
||||
setRealSecurityLevel(securityData);
|
||||
setLastSecurityUpdate(Date.now());
|
||||
console.log('✅ Header security level force-updated');
|
||||
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Force update failed:', error);
|
||||
|
||||
});
|
||||
} else {
|
||||
setLastSecurityUpdate(0);
|
||||
@@ -159,7 +158,7 @@ const EnhancedMinimalHeader = ({
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
}, [sessionTimeLeft]);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleForceUpdate = (event) => {
|
||||
@@ -171,9 +170,7 @@ const EnhancedMinimalHeader = ({
|
||||
|
||||
// Connection cleanup handler (use existing event from module)
|
||||
const handleConnectionCleaned = () => {
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('🧹 Connection cleaned - clearing security data in header');
|
||||
}
|
||||
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
@@ -184,9 +181,7 @@ const EnhancedMinimalHeader = ({
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = () => {
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('👋 Peer disconnect detected - clearing security data in header');
|
||||
}
|
||||
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
@@ -237,15 +232,12 @@ const EnhancedMinimalHeader = ({
|
||||
if (webrtcManager && window.EnhancedSecureCryptoUtils) {
|
||||
try {
|
||||
realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
|
||||
console.log('✅ Real security tests completed:', realTestResults);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Real security tests failed:', error);
|
||||
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Cannot run security tests:', {
|
||||
webrtcManager: !!webrtcManager,
|
||||
cryptoUtils: !!window.EnhancedSecureCryptoUtils
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// If no real test results and no existing security level, show progress message
|
||||
@@ -268,16 +260,14 @@ const EnhancedMinimalHeader = ({
|
||||
details: 'Security verification not available',
|
||||
isRealData: false,
|
||||
passedChecks: 0,
|
||||
totalChecks: 0,
|
||||
sessionType: 'unknown'
|
||||
totalChecks: 0
|
||||
};
|
||||
console.log('Using fallback security data:', securityData);
|
||||
|
||||
}
|
||||
|
||||
// Detailed information about the REAL security check
|
||||
let message = `REAL-TIME SECURITY VERIFICATION\n\n`;
|
||||
message += `Security Level: ${securityData.level} (${securityData.score}%)\n`;
|
||||
message += `Session Type: ${securityData.sessionType || 'premium'}\n`;
|
||||
message += `Verification Time: ${new Date(securityData.timestamp).toLocaleTimeString()}\n`;
|
||||
message += `Data Source: ${securityData.isRealData ? 'Real Cryptographic Tests' : 'Simulated Data'}\n\n`;
|
||||
|
||||
@@ -465,7 +455,6 @@ const EnhancedMinimalHeader = ({
|
||||
const config = getStatusConfig();
|
||||
const displaySecurityLevel = isConnected ? (realSecurityLevel || securityLevel) : null;
|
||||
|
||||
const shouldShowTimer = hasActiveSession && currentTimeLeft > 0 && window.SessionTimer;
|
||||
|
||||
// ============================================
|
||||
// DATA RELIABILITY INDICATOR
|
||||
@@ -505,18 +494,7 @@ const EnhancedMinimalHeader = ({
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
window.debugHeaderSecurity = () => {
|
||||
console.log('🔍 Header Security Debug:', {
|
||||
realSecurityLevel,
|
||||
lastSecurityUpdate,
|
||||
isConnected,
|
||||
webrtcManagerProp: !!webrtcManager,
|
||||
windowWebrtcManager: !!window.webrtcManager,
|
||||
cryptoUtils: !!window.EnhancedSecureCryptoUtils,
|
||||
displaySecurityLevel: displaySecurityLevel,
|
||||
securityDetails: securityDetails
|
||||
});
|
||||
};
|
||||
window.debugHeaderSecurity = undefined;
|
||||
|
||||
return () => {
|
||||
delete window.debugHeaderSecurity;
|
||||
@@ -561,7 +539,7 @@ const EnhancedMinimalHeader = ({
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom v4.3.120')
|
||||
}, 'End-to-end freedom v4.7.53')
|
||||
])
|
||||
]),
|
||||
|
||||
@@ -570,13 +548,6 @@ const EnhancedMinimalHeader = ({
|
||||
key: 'status-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
// Session Timer - all features enabled by default
|
||||
shouldShowTimer && React.createElement(window.SessionTimer, {
|
||||
key: 'session-timer',
|
||||
timeLeft: currentTimeLeft,
|
||||
sessionType: sessionType,
|
||||
onDisconnect: onDisconnect
|
||||
}),
|
||||
|
||||
displaySecurityLevel && React.createElement('div', {
|
||||
key: 'security-level',
|
||||
|
||||
@@ -75,11 +75,11 @@ function Roadmap() {
|
||||
|
||||
// current and future phases
|
||||
{
|
||||
version: "v4.3.120",
|
||||
version: "v4.5",
|
||||
title: "Enhanced Security Edition",
|
||||
status: "current",
|
||||
status: "done",
|
||||
date: "Now",
|
||||
description: "Current version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
|
||||
description: "Version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
|
||||
features: [
|
||||
"ECDH + DTLS + SAS triple-layer security",
|
||||
"ECDH P-384 + AES-GCM 256-bit encryption",
|
||||
@@ -96,17 +96,17 @@ function Roadmap() {
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v4.5",
|
||||
title: "Mobile & Desktop Edition",
|
||||
status: "development",
|
||||
date: "Q2 2025",
|
||||
description: "Native apps for all platforms",
|
||||
version: "v4.7",
|
||||
title: "Desktop Edition",
|
||||
status: "current",
|
||||
date: "Now",
|
||||
description: "Native desktop applications for Windows, macOS, and Linux",
|
||||
features: [
|
||||
"PWA app for mobile",
|
||||
"Electron app for desktop",
|
||||
"Windows desktop app (Tauri v2)",
|
||||
"macOS desktop app (Tauri v2)",
|
||||
"Linux AppImage support (Tauri v2)",
|
||||
"Real-time notifications",
|
||||
"Automatic reconnection",
|
||||
"Battery optimization",
|
||||
"Cross-device synchronization",
|
||||
"Improved UX/UI",
|
||||
"Support for files up to 100MB"
|
||||
@@ -114,9 +114,26 @@ function Roadmap() {
|
||||
},
|
||||
{
|
||||
version: "v5.0",
|
||||
title: "Mobile Edition",
|
||||
status: "development",
|
||||
date: "Q1 2026",
|
||||
description: "Native mobile applications for iOS and Android",
|
||||
features: [
|
||||
"iOS native app (Swift/SwiftUI)",
|
||||
"Android native app (Kotlin/Jetpack Compose)",
|
||||
"PWA support for mobile browsers",
|
||||
"Real-time push notifications",
|
||||
"Battery optimization",
|
||||
"Mobile-optimized UX/UI",
|
||||
"Offline message queuing",
|
||||
"Biometric authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.5",
|
||||
title: "Quantum-Resistant Edition",
|
||||
status: "planned",
|
||||
date: "Q4 2025",
|
||||
date: "Q2 2026",
|
||||
description: "Protection against quantum computers",
|
||||
features: [
|
||||
"Post-quantum cryptography CRYSTALS-Kyber",
|
||||
@@ -130,10 +147,10 @@ function Roadmap() {
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.5",
|
||||
version: "v6.0",
|
||||
title: "Group Communications",
|
||||
status: "planned",
|
||||
date: "Q2 2026",
|
||||
date: "Q4 2026",
|
||||
description: "Group chats with preserved privacy",
|
||||
features: [
|
||||
"P2P group connections up to 8 participants",
|
||||
@@ -146,7 +163,7 @@ function Roadmap() {
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v6.0",
|
||||
version: "v6.5",
|
||||
title: "Decentralized Network",
|
||||
status: "research",
|
||||
date: "2027",
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
// SessionTimer Component - v4.3.120 - ECDH + DTLS + SAS
|
||||
const SessionTimer = ({ timeLeft, sessionType, sessionManager, onDisconnect }) => {
|
||||
const [currentTime, setCurrentTime] = React.useState(timeLeft || 0);
|
||||
const [showExpiredMessage, setShowExpiredMessage] = React.useState(false);
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
const [connectionBroken, setConnectionBroken] = React.useState(false);
|
||||
|
||||
|
||||
const [loggedHidden, setLoggedHidden] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer initialization skipped - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let initialTime = 0;
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
initialTime = sessionManager.getTimeLeft();
|
||||
} else if (timeLeft && timeLeft > 0) {
|
||||
initialTime = timeLeft;
|
||||
}
|
||||
|
||||
if (initialTime <= 0) {
|
||||
setCurrentTime(0);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
setCurrentTime(0);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(true);
|
||||
return;
|
||||
}
|
||||
setCurrentTime(initialTime);
|
||||
setInitialized(true);
|
||||
setLoggedHidden(false);
|
||||
}, [sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeLeft && timeLeft > 0) {
|
||||
setCurrentTime(timeLeft);
|
||||
}
|
||||
setLoggedHidden(false);
|
||||
}, [timeLeft, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0 || !sessionManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (connectionBroken) {
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
|
||||
if (window.DEBUG_MODE && Math.floor(Date.now() / 30000) !== Math.floor((Date.now() - 1000) / 30000)) {
|
||||
console.log('⏱️ Timer tick:', Math.floor(newTime / 1000) + 's');
|
||||
}
|
||||
|
||||
if (newTime <= 0) {
|
||||
setShowExpiredMessage(true);
|
||||
setTimeout(() => setShowExpiredMessage(false), 5000);
|
||||
clearInterval(interval);
|
||||
}
|
||||
} else {
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [initialized, currentTime, sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSessionTimerUpdate = (event) => {
|
||||
if (connectionBroken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.detail.timeLeft && event.detail.timeLeft > 0) {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceHeaderUpdate = (event) => {
|
||||
if (connectionBroken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager && sessionManager.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
} else {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleNewConnection = (event) => {
|
||||
setConnectionBroken(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleConnectionCleaned = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleSessionReset = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleSessionCleanup = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleDisconnected = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
document.addEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.addEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.addEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.addEventListener('new-connection', handleNewConnection);
|
||||
document.addEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.addEventListener('session-reset', handleSessionReset);
|
||||
document.addEventListener('session-cleanup', handleSessionCleanup);
|
||||
document.addEventListener('disconnected', handleDisconnected);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.removeEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.removeEventListener('new-connection', handleNewConnection);
|
||||
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.removeEventListener('session-reset', handleSessionReset);
|
||||
document.removeEventListener('session-cleanup', handleSessionCleanup);
|
||||
document.removeEventListener('disconnected', handleDisconnected);
|
||||
};
|
||||
}, [sessionManager]);
|
||||
|
||||
if (showExpiredMessage) {
|
||||
return React.createElement('div', {
|
||||
className: 'session-timer expired flex items-center space-x-2 px-3 py-1.5 rounded-lg animate-pulse',
|
||||
style: { background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%)' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exclamation-triangle text-red-400'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'message',
|
||||
className: 'text-red-400 text-sm font-medium'
|
||||
}, 'Session Expired!')
|
||||
]);
|
||||
}
|
||||
|
||||
if (!sessionManager) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no sessionManager');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no time left, currentTime:', currentTime);
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loggedHidden) {
|
||||
setLoggedHidden(false);
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(currentTime / (60 * 1000));
|
||||
const totalSeconds = Math.floor(currentTime / 1000);
|
||||
|
||||
const isDemo = sessionType === 'demo';
|
||||
const isWarning = isDemo ? totalMinutes <= 2 : totalMinutes <= 10;
|
||||
const isCritical = isDemo ? totalSeconds <= 60 : totalMinutes <= 5;
|
||||
|
||||
const formatTime = (ms) => {
|
||||
const hours = Math.floor(ms / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
|
||||
const seconds = Math.floor((ms % (60 * 1000)) / 1000);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimerStyle = () => {
|
||||
const totalDuration = sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000;
|
||||
const timeProgress = (totalDuration - currentTime) / totalDuration;
|
||||
|
||||
let backgroundColor, textColor, iconColor, iconClass, shouldPulse;
|
||||
|
||||
if (timeProgress <= 0.33) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.15) 100%)';
|
||||
textColor = 'text-green-400';
|
||||
iconColor = 'text-green-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else if (timeProgress <= 0.66) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(234, 179, 8, 0.15) 0%, rgba(202, 138, 4, 0.15) 100%)';
|
||||
textColor = 'text-yellow-400';
|
||||
iconColor = 'text-yellow-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(220, 38, 38, 0.15) 100%)';
|
||||
textColor = 'text-red-400';
|
||||
iconColor = 'text-red-400';
|
||||
iconClass = 'fas fa-exclamation-triangle';
|
||||
shouldPulse = true;
|
||||
}
|
||||
|
||||
return { backgroundColor, textColor, iconColor, iconClass, shouldPulse };
|
||||
};
|
||||
|
||||
const timerStyle = getTimerStyle();
|
||||
|
||||
const handleTimerClick = () => {
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: `session-timer flex items-center space-x-2 px-3 py-1.5 rounded-lg transition-all duration-500 cursor-pointer hover:opacity-80 ${
|
||||
isDemo ? 'demo-session' : ''
|
||||
} ${timerStyle.shouldPulse ? 'animate-pulse' : ''}`,
|
||||
style: { background: timerStyle.backgroundColor },
|
||||
onClick: handleTimerClick,
|
||||
title: 'Click to disconnect and clear session'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${timerStyle.iconClass} ${timerStyle.iconColor}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'time',
|
||||
className: `text-sm font-mono font-semibold ${timerStyle.textColor}`
|
||||
}, formatTime(currentTime)),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: 'ml-2 w-16 h-1 bg-gray-700 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `${timerStyle.textColor.replace('text-', 'bg-')} h-full rounded-full transition-all duration-500`,
|
||||
style: {
|
||||
width: `${Math.max(0, Math.min(100, (currentTime / (sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000)) * 100))}%`
|
||||
}
|
||||
})
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.SessionTimer = SessionTimer;
|
||||
|
||||
window.updateSessionTimer = (newTimeLeft, newSessionType) => {
|
||||
document.dispatchEvent(new CustomEvent('session-timer-update', {
|
||||
detail: { timeLeft: newTimeLeft, sessionType: newSessionType }
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -128,7 +128,7 @@ class EnhancedSecureCryptoUtils {
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
iterations: 310000,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
keyMaterial,
|
||||
@@ -191,7 +191,7 @@ class EnhancedSecureCryptoUtils {
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
iterations: 310000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
@@ -224,13 +224,18 @@ class EnhancedSecureCryptoUtils {
|
||||
// Generate secure password for data exchange
|
||||
static generateSecurePassword() {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
const charCount = chars.length;
|
||||
const length = 32;
|
||||
const randomValues = new Uint32Array(length);
|
||||
crypto.getRandomValues(randomValues);
|
||||
|
||||
let password = '';
|
||||
|
||||
// Use rejection sampling to avoid bias
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += chars[randomValues[i] % chars.length];
|
||||
let randomValue;
|
||||
do {
|
||||
randomValue = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
} while (randomValue >= 4294967296 - (4294967296 % charCount)); // Reject biased values
|
||||
|
||||
password += chars[randomValue % charCount];
|
||||
}
|
||||
return password;
|
||||
}
|
||||
@@ -615,9 +620,7 @@ class EnhancedSecureCryptoUtils {
|
||||
|
||||
static async verifyReplayProtection(securityManager) {
|
||||
try {
|
||||
console.log('🔍 verifyReplayProtection debug:');
|
||||
console.log(' - securityManager.replayProtection:', securityManager.replayProtection);
|
||||
console.log(' - securityManager keys:', Object.keys(securityManager));
|
||||
// Debug logs removed to prevent leaking runtime state
|
||||
|
||||
// Check if replay protection is enabled
|
||||
if (!securityManager.replayProtection) {
|
||||
@@ -632,8 +635,7 @@ class EnhancedSecureCryptoUtils {
|
||||
|
||||
static async verifyDTLSFingerprint(securityManager) {
|
||||
try {
|
||||
console.log('🔍 verifyDTLSFingerprint debug:');
|
||||
console.log(' - securityManager.dtlsFingerprint:', securityManager.dtlsFingerprint);
|
||||
// Debug logs removed
|
||||
|
||||
// Check if DTLS fingerprint is available
|
||||
if (!securityManager.dtlsFingerprint) {
|
||||
@@ -648,8 +650,7 @@ class EnhancedSecureCryptoUtils {
|
||||
|
||||
static async verifySASVerification(securityManager) {
|
||||
try {
|
||||
console.log('🔍 verifySASVerification debug:');
|
||||
console.log(' - securityManager.sasCode:', securityManager.sasCode);
|
||||
// Debug logs removed
|
||||
|
||||
// Check if SAS code is available
|
||||
if (!securityManager.sasCode) {
|
||||
@@ -664,8 +665,7 @@ class EnhancedSecureCryptoUtils {
|
||||
|
||||
static async verifyTrafficObfuscation(securityManager) {
|
||||
try {
|
||||
console.log('🔍 verifyTrafficObfuscation debug:');
|
||||
console.log(' - securityManager.trafficObfuscation:', securityManager.trafficObfuscation);
|
||||
// Debug logs removed
|
||||
|
||||
// Check if traffic obfuscation is enabled
|
||||
if (!securityManager.trafficObfuscation) {
|
||||
@@ -972,11 +972,18 @@ class EnhancedSecureCryptoUtils {
|
||||
if (level === 'error') {
|
||||
// В production показываем только код ошибки без деталей
|
||||
console.error(`❌ [SecureChat] ${message} [ERROR_CODE: ${this._generateErrorCode(message)}]`);
|
||||
// Временно показываем детали для отладки
|
||||
if (context && Object.keys(context).length > 0) {
|
||||
console.error('Error details:', context);
|
||||
}
|
||||
} else if (level === 'warn') {
|
||||
// В production показываем только предупреждение без контекста
|
||||
console.warn(`⚠️ [SecureChat] ${message}`);
|
||||
} else if (level === 'info' || level === 'debug') {
|
||||
// Временно показываем info/debug логи для отладки
|
||||
console.log(`[SecureChat] ${message}`, context);
|
||||
} else {
|
||||
// В production не показываем info/debug логи
|
||||
// В production не показываем другие логи
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -1085,14 +1092,11 @@ class EnhancedSecureCryptoUtils {
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDH key pair generated successfully (P-384)', {
|
||||
curve: 'P-384',
|
||||
extractable: false
|
||||
});
|
||||
// Removed key generation info logging to avoid exposing key-related metadata
|
||||
|
||||
return keyPair;
|
||||
} catch (p384Error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 generation failed, trying P-256', { error: p384Error.message });
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 generation failed, switching curve', { error: p384Error.message });
|
||||
|
||||
// Fallback to P-256
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
@@ -1104,10 +1108,7 @@ class EnhancedSecureCryptoUtils {
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDH key pair generated successfully (P-256 fallback)', {
|
||||
curve: 'P-256',
|
||||
extractable: false
|
||||
});
|
||||
// Removed key generation info logging to avoid exposing key-related metadata
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
@@ -1131,14 +1132,11 @@ class EnhancedSecureCryptoUtils {
|
||||
['sign', 'verify']
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA key pair generated successfully (P-384)', {
|
||||
curve: 'P-384',
|
||||
extractable: false
|
||||
});
|
||||
// Removed key generation info logging to avoid exposing key-related metadata
|
||||
|
||||
return keyPair;
|
||||
} catch (p384Error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 generation failed, trying P-256', { error: p384Error.message });
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 generation failed, switching curve', { error: p384Error.message });
|
||||
|
||||
// Fallback to P-256
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
@@ -1150,10 +1148,7 @@ class EnhancedSecureCryptoUtils {
|
||||
['sign', 'verify']
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA key pair generated successfully (P-256 fallback)', {
|
||||
curve: 'P-256',
|
||||
extractable: false
|
||||
});
|
||||
// Removed key generation info logging to avoid exposing key-related metadata
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
@@ -1204,12 +1199,17 @@ class EnhancedSecureCryptoUtils {
|
||||
// Verify ECDSA signature (P-384 or P-256)
|
||||
static async verifySignature(publicKey, signature, data) {
|
||||
try {
|
||||
// Debug logs removed
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;
|
||||
const signatureBuffer = new Uint8Array(signature);
|
||||
|
||||
// Debug logs removed
|
||||
|
||||
// Try SHA-384 first, fallback to SHA-256
|
||||
try {
|
||||
// Debug logs removed
|
||||
const isValid = await crypto.subtle.verify(
|
||||
{
|
||||
name: 'ECDSA',
|
||||
@@ -1220,15 +1220,16 @@ class EnhancedSecureCryptoUtils {
|
||||
dataBuffer
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signature verification completed (SHA-384)', {
|
||||
isValid,
|
||||
dataSize: dataBuffer.length
|
||||
});
|
||||
// Debug logs removed
|
||||
|
||||
// Removed signature verification info logging
|
||||
|
||||
return isValid;
|
||||
} catch (sha384Error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 verification failed, trying SHA-256', { error: sha384Error.message });
|
||||
// Debug logs removed
|
||||
// Removed signature verification transition logging
|
||||
|
||||
// Debug logs removed
|
||||
const isValid = await crypto.subtle.verify(
|
||||
{
|
||||
name: 'ECDSA',
|
||||
@@ -1239,10 +1240,9 @@ class EnhancedSecureCryptoUtils {
|
||||
dataBuffer
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signature verification completed (SHA-256 fallback)', {
|
||||
isValid,
|
||||
dataSize: dataBuffer.length
|
||||
});
|
||||
// Debug logs removed
|
||||
|
||||
// Removed signature verification info logging
|
||||
|
||||
return isValid;
|
||||
}
|
||||
@@ -1338,10 +1338,7 @@ class EnhancedSecureCryptoUtils {
|
||||
throw new Error(`Invalid or unsupported curve OID: ${curveOidString}`);
|
||||
}
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'EC key curve validated', {
|
||||
curve: validCurves[curveOidString],
|
||||
oid: curveOidString
|
||||
});
|
||||
// Removed curve validation info logging
|
||||
}
|
||||
|
||||
// Validate public key BIT STRING
|
||||
@@ -1404,13 +1401,7 @@ class EnhancedSecureCryptoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Key structure validation passed', {
|
||||
keyLen: keyBytes.length,
|
||||
algorithm: expectedAlgorithm,
|
||||
asn1Valid: true,
|
||||
oidValid: true,
|
||||
importValid: true
|
||||
});
|
||||
// Removed key structure validation info logging
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -1564,11 +1555,7 @@ class EnhancedSecureCryptoUtils {
|
||||
signature
|
||||
};
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Public key exported with signature', {
|
||||
keyType,
|
||||
keySize: keyData.length,
|
||||
signed: true
|
||||
});
|
||||
// Removed public key export with signature info logging
|
||||
|
||||
return signedPackage;
|
||||
} catch (error) {
|
||||
@@ -1583,6 +1570,8 @@ class EnhancedSecureCryptoUtils {
|
||||
// Import and verify signed public key
|
||||
static async importSignedPublicKey(signedPackage, verifyingKey, expectedKeyType = 'ECDH') {
|
||||
try {
|
||||
// Debug logs removed
|
||||
|
||||
// Validate package structure
|
||||
if (!signedPackage || typeof signedPackage !== 'object') {
|
||||
throw new Error('Invalid signed package format');
|
||||
@@ -1609,7 +1598,9 @@ class EnhancedSecureCryptoUtils {
|
||||
// Verify signature
|
||||
const packageCopy = { keyType, keyData, timestamp, version };
|
||||
const packageString = JSON.stringify(packageCopy);
|
||||
// Debug logs removed
|
||||
const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signature, packageString);
|
||||
// Debug logs removed
|
||||
|
||||
if (!isValidSignature) {
|
||||
throw new Error('Invalid signature on key package - possible MITM attack');
|
||||
@@ -1634,18 +1625,12 @@ class EnhancedSecureCryptoUtils {
|
||||
keyUsages
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully (P-384)', {
|
||||
keyType,
|
||||
signatureValid: true,
|
||||
keyAge: Math.round(keyAge / 1000) + 's'
|
||||
});
|
||||
// Removed public key import info logging
|
||||
|
||||
return publicKey;
|
||||
} catch (p384Error) {
|
||||
// Fallback to P-256
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', {
|
||||
error: p384Error.message
|
||||
});
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 import failed, switching curve', { error: p384Error.message });
|
||||
|
||||
const algorithm = keyType === 'ECDH' ?
|
||||
{ name: 'ECDH', namedCurve: 'P-256' }
|
||||
@@ -1661,11 +1646,7 @@ class EnhancedSecureCryptoUtils {
|
||||
keyUsages
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully (P-256 fallback)', {
|
||||
keyType,
|
||||
signatureValid: true,
|
||||
keyAge: Math.round(keyAge / 1000) + 's'
|
||||
});
|
||||
// Removed public key import info logging
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
@@ -1686,7 +1667,7 @@ class EnhancedSecureCryptoUtils {
|
||||
|
||||
await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key exported', { keySize: keyData.length });
|
||||
// Removed legacy public key export info logging
|
||||
return keyData;
|
||||
} catch (error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key export failed', { error: error.message });
|
||||
@@ -1714,7 +1695,7 @@ class EnhancedSecureCryptoUtils {
|
||||
[]
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key imported (P-384)', { keySize: keyData.length });
|
||||
// Removed legacy public key import info logging
|
||||
return publicKey;
|
||||
} catch (p384Error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });
|
||||
@@ -1731,7 +1712,7 @@ class EnhancedSecureCryptoUtils {
|
||||
[]
|
||||
);
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key imported (P-256 fallback)', { keySize: keyData.length });
|
||||
// Removed legacy public key import info logging
|
||||
return publicKey;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1812,15 +1793,7 @@ class EnhancedSecureCryptoUtils {
|
||||
const keyFingerprint = await EnhancedSecureCryptoUtils.calculateKeyFingerprint(signedPackage.keyData);
|
||||
|
||||
// Log successful verification with security details
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'SECURE: Signature verification passed for signed package', {
|
||||
keyType: signedPackage.keyType,
|
||||
keySize: signedPackage.keyData.length,
|
||||
timestamp: signedPackage.timestamp,
|
||||
version: signedPackage.version,
|
||||
signatureVerified: true,
|
||||
securityLevel: 'HIGH',
|
||||
keyFingerprint: keyFingerprint.substring(0, 8) // Only log first 8 chars for security
|
||||
});
|
||||
// Removed signature verification pass details to avoid key-related logging
|
||||
|
||||
// Import the public key with fallback
|
||||
const keyBytes = new Uint8Array(signedPackage.keyData);
|
||||
@@ -1883,6 +1856,8 @@ class EnhancedSecureCryptoUtils {
|
||||
// Enhanced key derivation with metadata protection and 64-byte salt
|
||||
static async deriveSharedKeys(privateKey, publicKey, salt) {
|
||||
try {
|
||||
// Removed detailed key derivation logging
|
||||
|
||||
// Validate input parameters are CryptoKey instances
|
||||
if (!(privateKey instanceof CryptoKey)) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Private key is not a CryptoKey', {
|
||||
@@ -1897,7 +1872,7 @@ class EnhancedSecureCryptoUtils {
|
||||
publicKeyType: typeof publicKey,
|
||||
publicKeyAlgorithm: publicKey?.algorithm?.name
|
||||
});
|
||||
throw new Error('The private key is not a valid CryptoKey.');
|
||||
throw new Error('The public key is not a valid CryptoKey.');
|
||||
}
|
||||
|
||||
// Validate salt size (should be 64 bytes for enhanced security)
|
||||
@@ -1908,209 +1883,156 @@ class EnhancedSecureCryptoUtils {
|
||||
const saltBytes = new Uint8Array(salt);
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// Enhanced context info with version and additional entropy
|
||||
const contextInfo = encoder.encode('SecureBit.chat v4.0 Enhanced Security Edition');
|
||||
|
||||
// Derive master shared secret with enhanced parameters
|
||||
// Try SHA-384 first, fallback to SHA-256
|
||||
let sharedSecret;
|
||||
// Step 1: Derive raw ECDH shared secret using pure ECDH
|
||||
let rawSharedSecret;
|
||||
try {
|
||||
sharedSecret = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'ECDH',
|
||||
public: publicKey
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: saltBytes,
|
||||
info: contextInfo
|
||||
},
|
||||
false, // Non-extractable
|
||||
['deriveKey']
|
||||
);
|
||||
} catch (sha384Error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 key derivation failed, trying SHA-256', {
|
||||
error: sha384Error.message,
|
||||
privateKeyType: typeof privateKey,
|
||||
publicKeyType: typeof publicKey,
|
||||
privateKeyAlgorithm: privateKey?.algorithm?.name,
|
||||
publicKeyAlgorithm: publicKey?.algorithm?.name
|
||||
});
|
||||
// Removed detailed key derivation logging
|
||||
|
||||
sharedSecret = await crypto.subtle.deriveKey(
|
||||
// Use pure ECDH to derive raw key material
|
||||
const rawKeyMaterial = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'ECDH',
|
||||
public: publicKey
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: contextInfo
|
||||
},
|
||||
false, // Non-extractable
|
||||
['deriveKey']
|
||||
);
|
||||
}
|
||||
|
||||
// Derive message encryption key with fallback
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-encryption-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable for enhanced security
|
||||
true, // Extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
} catch (sha384Error) {
|
||||
encryptionKey = await crypto.subtle.deriveKey(
|
||||
|
||||
// Export the raw key material
|
||||
const rawKeyData = await crypto.subtle.exportKey('raw', rawKeyMaterial);
|
||||
|
||||
// Import as HKDF key material for further derivation
|
||||
rawSharedSecret = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
rawKeyData,
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-encryption-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable for enhanced security
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
// Derive MAC key for message authentication with fallback
|
||||
let macKey;
|
||||
try {
|
||||
macKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-authentication-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-384'
|
||||
},
|
||||
false, // Non-extractable
|
||||
['sign', 'verify']
|
||||
);
|
||||
} catch (sha384Error) {
|
||||
macKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-authentication-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
false, // Non-extractable
|
||||
['sign', 'verify']
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
// Removed detailed key derivation logging
|
||||
} catch (error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH derivation failed', {
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Step 2: Use HKDF to derive specific keys directly
|
||||
// Removed detailed key derivation logging
|
||||
|
||||
// Derive separate metadata encryption key with fallback
|
||||
// Step 3: Derive specific keys using HKDF with unique info parameters
|
||||
// Each key uses unique info parameter for proper separation
|
||||
|
||||
// Derive message encryption key (messageKey)
|
||||
let messageKey;
|
||||
messageKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-encryption-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable for enhanced security
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Derive MAC key for message authentication
|
||||
let macKey;
|
||||
macKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('message-authentication-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
false, // Non-extractable
|
||||
['sign', 'verify']
|
||||
);
|
||||
|
||||
// Derive Perfect Forward Secrecy key (pfsKey)
|
||||
let pfsKey;
|
||||
pfsKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('perfect-forward-secrecy-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Derive separate metadata encryption key
|
||||
let metadataKey;
|
||||
try {
|
||||
metadataKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('metadata-protection-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
} catch (sha384Error) {
|
||||
metadataKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('metadata-protection-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
metadataKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('metadata-protection-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false, // Non-extractable
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Generate temporary extractable key for fingerprint calculation with fallback
|
||||
// Generate temporary extractable key for fingerprint calculation
|
||||
let fingerprintKey;
|
||||
try {
|
||||
fingerprintKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('fingerprint-generation-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
true, // Extractable only for fingerprint
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
} catch (sha384Error) {
|
||||
fingerprintKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('fingerprint-generation-v4')
|
||||
},
|
||||
sharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
true, // Extractable only for fingerprint
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
fingerprintKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: saltBytes,
|
||||
info: encoder.encode('fingerprint-generation-v4')
|
||||
},
|
||||
rawSharedSecret,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
true, // Extractable only for fingerprint
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Generate key fingerprint for verification
|
||||
const fingerprintKeyData = await crypto.subtle.exportKey('raw', fingerprintKey);
|
||||
const fingerprint = await EnhancedSecureCryptoUtils.generateKeyFingerprint(Array.from(new Uint8Array(fingerprintKeyData)));
|
||||
|
||||
// Validate that all derived keys are CryptoKey instances
|
||||
if (!(encryptionKey instanceof CryptoKey)) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived encryption key is not a CryptoKey', {
|
||||
encryptionKeyType: typeof encryptionKey,
|
||||
encryptionKeyAlgorithm: encryptionKey?.algorithm?.name
|
||||
if (!(messageKey instanceof CryptoKey)) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived message key is not a CryptoKey', {
|
||||
messageKeyType: typeof messageKey,
|
||||
messageKeyAlgorithm: messageKey?.algorithm?.name
|
||||
});
|
||||
throw new Error('The derived encryption key is not a valid CryptoKey.');
|
||||
throw new Error('The derived message key is not a valid CryptoKey.');
|
||||
}
|
||||
|
||||
if (!(macKey instanceof CryptoKey)) {
|
||||
@@ -2121,6 +2043,14 @@ class EnhancedSecureCryptoUtils {
|
||||
throw new Error('The derived MAC key is not a valid CryptoKey.');
|
||||
}
|
||||
|
||||
if (!(pfsKey instanceof CryptoKey)) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived PFS key is not a CryptoKey', {
|
||||
pfsKeyType: typeof pfsKey,
|
||||
pfsKeyAlgorithm: pfsKey?.algorithm?.name
|
||||
});
|
||||
throw new Error('The derived PFS key is not a valid CryptoKey.');
|
||||
}
|
||||
|
||||
if (!(metadataKey instanceof CryptoKey)) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived metadata key is not a CryptoKey', {
|
||||
metadataKeyType: typeof metadataKey,
|
||||
@@ -2129,24 +2059,27 @@ class EnhancedSecureCryptoUtils {
|
||||
throw new Error('The derived metadata key is not a valid CryptoKey.');
|
||||
}
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced shared keys derived successfully', {
|
||||
saltSize: salt.length,
|
||||
hasMetadataKey: true,
|
||||
nonExtractable: true,
|
||||
version: '4.0',
|
||||
allKeysValid: true
|
||||
});
|
||||
// Removed detailed key derivation success logging
|
||||
|
||||
return {
|
||||
encryptionKey,
|
||||
messageKey, // Renamed from encryptionKey for clarity
|
||||
macKey,
|
||||
pfsKey, // Added Perfect Forward Secrecy key
|
||||
metadataKey,
|
||||
fingerprint,
|
||||
timestamp: Date.now(),
|
||||
version: '4.0'
|
||||
};
|
||||
} catch (error) {
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced key derivation failed', { error: error.message });
|
||||
EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced key derivation failed', {
|
||||
error: error.message,
|
||||
errorStack: error.stack,
|
||||
privateKeyType: typeof privateKey,
|
||||
publicKeyType: typeof publicKey,
|
||||
saltLength: salt?.length,
|
||||
privateKeyAlgorithm: privateKey?.algorithm?.name,
|
||||
publicKeyAlgorithm: publicKey?.algorithm?.name
|
||||
});
|
||||
throw new Error(`Failed to create shared encryption keys: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -2241,9 +2174,9 @@ class EnhancedSecureCryptoUtils {
|
||||
throw new Error('Challenge mismatch - possible replay attack');
|
||||
}
|
||||
|
||||
// Check response time (max 5 minutes)
|
||||
// Check response time (max 30 minutes for better UX)
|
||||
const responseAge = Date.now() - proof.responseTimestamp;
|
||||
if (responseAge > 300000) {
|
||||
if (responseAge > 1800000) {
|
||||
throw new Error('Proof response expired');
|
||||
}
|
||||
|
||||
@@ -2296,11 +2229,19 @@ class EnhancedSecureCryptoUtils {
|
||||
// Generate verification code for out-of-band authentication
|
||||
static generateVerificationCode() {
|
||||
const chars = '0123456789ABCDEF';
|
||||
const charCount = chars.length;
|
||||
let result = '';
|
||||
const values = crypto.getRandomValues(new Uint8Array(6));
|
||||
|
||||
// Use rejection sampling to avoid bias
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars[values[i] % chars.length];
|
||||
let randomByte;
|
||||
do {
|
||||
randomByte = crypto.getRandomValues(new Uint8Array(1))[0];
|
||||
} while (randomByte >= 256 - (256 % charCount)); // Reject biased values
|
||||
|
||||
result += chars[randomByte % charCount];
|
||||
}
|
||||
|
||||
return result.match(/.{1,2}/g).join('-');
|
||||
}
|
||||
|
||||
@@ -2367,12 +2308,7 @@ class EnhancedSecureCryptoUtils {
|
||||
|
||||
payload.mac = Array.from(new Uint8Array(mac));
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Message encrypted with metadata protection', {
|
||||
messageId,
|
||||
sequenceNumber,
|
||||
hasMetadataProtection: true,
|
||||
hasPadding: true
|
||||
});
|
||||
// Logging removed to avoid noisy console output
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
@@ -2435,7 +2371,7 @@ class EnhancedSecureCryptoUtils {
|
||||
}
|
||||
|
||||
const messageAge = Date.now() - metadata.timestamp;
|
||||
if (messageAge > 300000) {
|
||||
if (messageAge > 1800000) { // 30 minutes for better UX
|
||||
throw new Error('Message expired (older than 5 minutes)');
|
||||
}
|
||||
|
||||
@@ -2484,22 +2420,86 @@ class EnhancedSecureCryptoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced input sanitization
|
||||
// Enhanced input sanitization with iterative processing to handle edge cases
|
||||
static sanitizeMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
throw new Error('Message must be a string');
|
||||
}
|
||||
|
||||
return message
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/data:/gi, '')
|
||||
.replace(/vbscript:/gi, '')
|
||||
.replace(/onload\s*=/gi, '')
|
||||
.replace(/onerror\s*=/gi, '')
|
||||
.replace(/onclick\s*=/gi, '')
|
||||
.trim()
|
||||
.substring(0, 2000); // Increased limit
|
||||
// Helper function to apply replacement until stable
|
||||
function replaceUntilStable(str, pattern, replacement = '') {
|
||||
let previous;
|
||||
do {
|
||||
previous = str;
|
||||
str = str.replace(pattern, replacement);
|
||||
} while (str !== previous);
|
||||
return str;
|
||||
}
|
||||
|
||||
// Define all dangerous patterns that need to be removed
|
||||
const dangerousPatterns = [
|
||||
// Script tags with various formats
|
||||
/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi,
|
||||
/<script\b[^>]*>[\s\S]*?<\/script\s+[^>]*>/gi,
|
||||
/<script\b[^>]*>[\s\S]*$/gi,
|
||||
// Other dangerous tags
|
||||
/<iframe\b[^>]*>[\s\S]*?<\/iframe\s*>/gi,
|
||||
/<object\b[^>]*>[\s\S]*?<\/object\s*>/gi,
|
||||
/<embed\b[^>]*>/gi,
|
||||
/<applet\b[^>]*>[\s\S]*?<\/applet\s*>/gi,
|
||||
/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi,
|
||||
// Dangerous protocols
|
||||
/javascript\s*:/gi,
|
||||
/data\s*:/gi,
|
||||
/vbscript\s*:/gi,
|
||||
// Event handlers
|
||||
/on\w+\s*=/gi,
|
||||
// HTML comments
|
||||
/<!--[\s\S]*?-->/g,
|
||||
// Link and meta tags with javascript
|
||||
/<link\b[^>]*javascript[^>]*>/gi,
|
||||
/<meta\b[^>]*javascript[^>]*>/gi,
|
||||
// Any remaining script-like content
|
||||
/<[^>]*script[^>]*>/gi,
|
||||
/<[^>]*on\w+\s*=[^>]*>/gi
|
||||
];
|
||||
|
||||
// Iterative sanitization to handle edge cases
|
||||
let sanitized = message;
|
||||
let previousLength;
|
||||
let iterations = 0;
|
||||
const maxIterations = 10; // Prevent infinite loops
|
||||
|
||||
do {
|
||||
previousLength = sanitized.length;
|
||||
|
||||
// Apply all dangerous patterns with stable replacement
|
||||
for (const pattern of dangerousPatterns) {
|
||||
sanitized = replaceUntilStable(sanitized, pattern);
|
||||
}
|
||||
|
||||
// Additional cleanup for edge cases - each applied until stable
|
||||
sanitized = replaceUntilStable(sanitized, /<[^>]*>/g);
|
||||
sanitized = replaceUntilStable(sanitized, /^\w+:/gi);
|
||||
sanitized = replaceUntilStable(sanitized, /\bon\w+\s*=\s*["'][^"']*["']/gi);
|
||||
sanitized = replaceUntilStable(sanitized, /\bon\w+\s*=\s*[^>\s]+/gi);
|
||||
|
||||
// Single character removal is inherently safe
|
||||
sanitized = sanitized.replace(/[<>]/g, '').trim();
|
||||
|
||||
iterations++;
|
||||
} while (sanitized.length !== previousLength && iterations < maxIterations);
|
||||
|
||||
// Final security pass with stable replacements
|
||||
sanitized = replaceUntilStable(sanitized, /<[^>]*>/g);
|
||||
sanitized = replaceUntilStable(sanitized, /^\w+:/gi);
|
||||
sanitized = replaceUntilStable(sanitized, /\bon\w+\s*=\s*["'][^"']*["']/gi);
|
||||
sanitized = replaceUntilStable(sanitized, /\bon\w+\s*=\s*[^>\s]+/gi);
|
||||
|
||||
// Final single character cleanup
|
||||
sanitized = sanitized.replace(/[<>]/g, '').trim();
|
||||
|
||||
return sanitized.substring(0, 2000); // Limit length
|
||||
}
|
||||
|
||||
// Generate cryptographically secure salt (64 bytes for enhanced security)
|
||||
@@ -2520,10 +2520,7 @@ class EnhancedSecureCryptoUtils {
|
||||
// Convert to hexadecimal string
|
||||
const fingerprint = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
EnhancedSecureCryptoUtils.secureLog.log('info', 'Key fingerprint calculated', {
|
||||
keySize: keyData.length,
|
||||
fingerprintLength: fingerprint.length
|
||||
});
|
||||
// Removed key fingerprint logging
|
||||
|
||||
return fingerprint;
|
||||
} catch (error) {
|
||||
|
||||
@@ -101,7 +101,7 @@ class EnhancedSecureWebRTCManager {
|
||||
};
|
||||
|
||||
// Static debug flag instead of this._debugMode
|
||||
static DEBUG_MODE = false; // Set to true during development, false in production
|
||||
static DEBUG_MODE = true; // Set to true during development, false in production
|
||||
|
||||
|
||||
constructor(onMessage, onStatusChange, onKeyExchange, onVerificationRequired, onAnswerError = null, onVerificationStateChange = null, config = {}) {
|
||||
@@ -167,8 +167,6 @@ class EnhancedSecureWebRTCManager {
|
||||
} : null;
|
||||
};
|
||||
this._secureLog('info', '🔒 Enhanced WebRTC Manager initialized with secure API');
|
||||
this.currentSessionType = null;
|
||||
this.currentSecurityLevel = 'basic';
|
||||
this.sessionConstraints = null;
|
||||
this.peerConnection = null;
|
||||
this.dataChannel = null;
|
||||
@@ -332,7 +330,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
this.onFileError = null;
|
||||
|
||||
// PFS (Perfect Forward Secrecy) Implementation
|
||||
this.keyRotationInterval = EnhancedSecureWebRTCManager.TIMEOUTS.KEY_ROTATION_INTERVAL;
|
||||
this.keyRotationInterval = null; // отключаем таймерную ротацию
|
||||
this.lastKeyRotation = Date.now();
|
||||
this.currentKeyVersion = 0;
|
||||
this.keyVersions = new Map(); // Store key versions for PFS
|
||||
@@ -374,6 +372,9 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
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
|
||||
this._hardenDebugModeReferences();
|
||||
|
||||
@@ -406,12 +407,13 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
// 3. Fake Traffic Generation
|
||||
this.fakeTrafficConfig = {
|
||||
enabled: this._config.fakeTraffic.enabled,
|
||||
minInterval: this._config.fakeTraffic.minInterval,
|
||||
maxInterval: this._config.fakeTraffic.maxInterval,
|
||||
minSize: this._config.fakeTraffic.minSize,
|
||||
maxSize: this._config.fakeTraffic.maxSize,
|
||||
patterns: this._config.fakeTraffic.patterns
|
||||
enabled: this._config.fakeTraffic?.enabled || false,
|
||||
minInterval: this._config.fakeTraffic?.minInterval || 15000,
|
||||
maxInterval: this._config.fakeTraffic?.maxInterval || 30000,
|
||||
minSize: this._config.fakeTraffic?.minSize || 64,
|
||||
maxSize: this._config.fakeTraffic?.maxSize || 1024,
|
||||
patterns: this._config.fakeTraffic?.patterns || ['heartbeat', 'status', 'ping'],
|
||||
randomDecoyIntervals: this._config.fakeTraffic?.randomDecoyIntervals || true
|
||||
};
|
||||
this.fakeTrafficTimer = null;
|
||||
this.lastFakeTraffic = 0;
|
||||
@@ -1184,9 +1186,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
this._secureLog('warn', '⚠️ High number of active keys detected. Consider rotation.');
|
||||
}
|
||||
|
||||
if (Date.now() - (this._keyStorageStats.lastRotation || 0) > 3600000) {
|
||||
this._rotateKeys();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1771,8 +1770,20 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
// Malicious pattern detection
|
||||
this._maliciousPatterns = [
|
||||
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, // Script tags
|
||||
/javascript:/gi, // JavaScript protocol
|
||||
// Enhanced script tag detection that handles edge cases
|
||||
/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, // Standard </script>
|
||||
/<script\b[^>]*>[\s\S]*?<\/script\s+[^>]*>/gi, // </script with attributes>
|
||||
/<script\b[^>]*>[\s\S]*$/gi, // Malformed script tags without closing
|
||||
// Additional dangerous tags
|
||||
/<iframe\b[^>]*>[\s\S]*?<\/iframe\s*>/gi, // iframe tags
|
||||
/<object\b[^>]*>[\s\S]*?<\/object\s*>/gi, // object tags
|
||||
/<embed\b[^>]*>/gi, // embed tags
|
||||
/<applet\b[^>]*>[\s\S]*?<\/applet\s*>/gi, // applet tags
|
||||
/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, // style tags
|
||||
// Dangerous protocols
|
||||
/javascript\s*:/gi, // JavaScript protocol
|
||||
/data\s*:/gi, // Data protocol
|
||||
/vbscript\s*:/gi, // VBScript protocol
|
||||
/data:text\/html/gi, // Data URLs with HTML
|
||||
/on\w+\s*=/gi, // Event handlers
|
||||
/eval\s*\(/gi, // eval() calls
|
||||
@@ -2311,7 +2322,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
// Create simple getSecurityStatus method
|
||||
secureAPI.getSecurityStatus = () => ({
|
||||
securityLevel: this.currentSecurityLevel || 'basic',
|
||||
securityLevel: 'maximum',
|
||||
stage: 'initialized',
|
||||
activeFeaturesCount: Object.values(this.securityFeatures || {}).filter(Boolean).length
|
||||
});
|
||||
@@ -2889,8 +2900,13 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
try {
|
||||
this._secureMemoryManager.isCleaning = true;
|
||||
|
||||
// Clean up any remaining sensitive data
|
||||
this._secureCleanupCryptographicMaterials();
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Clean up message queue if it's too large
|
||||
if (this.messageQueue && this.messageQueue.length > 100) {
|
||||
@@ -3451,7 +3467,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
// Validate timestamp (prevent very old messages)
|
||||
const now = Date.now();
|
||||
const messageAge = now - aad.timestamp;
|
||||
if (messageAge > 300000) { // 5 minutes
|
||||
if (messageAge > 1800000) { // 30 minutes for better UX
|
||||
throw new Error('AAD timestamp too old - possible replay attack');
|
||||
}
|
||||
|
||||
@@ -3534,11 +3550,19 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
const normalizedReceived = receivedFingerprint.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) {
|
||||
this._secureLog('error', 'DTLS fingerprint mismatch - possible MITM attack', {
|
||||
context: context,
|
||||
receivedHash: await this._createSafeLogHash(normalizedReceived, 'dtls_fingerprint'),
|
||||
expectedHash: await this._createSafeLogHash(normalizedExpected, 'dtls_fingerprint'),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
@@ -3547,7 +3571,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
this._secureLog('info', 'DTLS fingerprint validation successful', {
|
||||
context: context,
|
||||
fingerprintHash: await this._createSafeLogHash(normalizedReceived, 'dtls_fingerprint'),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
@@ -3622,7 +3645,14 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
const dv = new DataView(bits);
|
||||
const n = (dv.getUint32(0) ^ dv.getUint32(4)) >>> 0;
|
||||
const sasCode = String(n % 10_000_000).padStart(7, '0');
|
||||
|
||||
// Use rejection sampling to avoid bias in SAS code generation
|
||||
let sasValue;
|
||||
do {
|
||||
sasValue = crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
} while (sasValue >= 4294967296 - (4294967296 % 10_000_000));
|
||||
|
||||
const sasCode = String(sasValue % 10_000_000).padStart(7, '0');
|
||||
|
||||
|
||||
this._secureLog('info', 'SAS code computed successfully', {
|
||||
@@ -3811,7 +3841,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
});
|
||||
|
||||
this._secureLog('info', '✅ Ephemeral ECDH keys generated for PFS', {
|
||||
sessionIdHash: await this._createSafeLogHash(sessionId, 'session_id'),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
@@ -4000,6 +4029,46 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
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
|
||||
* @param {string|object} data - message payload
|
||||
@@ -4463,32 +4532,69 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fingerprint mask for anti-fingerprinting with enhanced randomization
|
||||
generateFingerprintMask() {
|
||||
// Enhanced randomization to prevent side-channel attacks
|
||||
const cryptoRandom = crypto.getRandomValues(new Uint8Array(128));
|
||||
|
||||
const mask = {
|
||||
timingOffset: cryptoRandom[0] % 1000 + cryptoRandom[1] % 500, // 0-1500ms
|
||||
sizeVariation: (cryptoRandom[2] % 50 + 75) / 100, // 0.75 to 1.25
|
||||
noisePattern: Array.from(crypto.getRandomValues(new Uint8Array(64))), // Increased size
|
||||
headerVariations: [
|
||||
'X-Client-Version', 'X-Session-ID', 'X-Request-ID', 'X-Timestamp', 'X-Signature',
|
||||
'X-Secure', 'X-Encrypted', 'X-Protected', 'X-Safe', 'X-Anonymous', 'X-Private'
|
||||
],
|
||||
noiseIntensity: cryptoRandom[3] % 100 + 50, // 50-150%
|
||||
sizeMultiplier: (cryptoRandom[4] % 50 + 75) / 100, // 0.75-1.25
|
||||
timingVariation: cryptoRandom[5] % 1000 + 100 // 100-1100ms
|
||||
};
|
||||
return mask;
|
||||
}
|
||||
// Helper function: unbiased integer in [min, max]
|
||||
getSafeRandomInt(min, max) {
|
||||
if (!Number.isInteger(min) || !Number.isInteger(max)) {
|
||||
throw new Error('getSafeRandomInt requires integer min and max');
|
||||
}
|
||||
if (min >= max) {
|
||||
throw new Error('min must be less than max');
|
||||
}
|
||||
|
||||
const range = max - min + 1;
|
||||
const bitsNeeded = Math.ceil(Math.log2(range));
|
||||
const bytesNeeded = Math.ceil(bitsNeeded / 8);
|
||||
const mask = (1 << bitsNeeded) - 1;
|
||||
|
||||
let randomValue;
|
||||
do {
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(bytesNeeded));
|
||||
|
||||
randomValue = 0;
|
||||
for (let i = 0; i < bytesNeeded; i++) {
|
||||
randomValue = (randomValue * 256) + randomBytes[i];
|
||||
}
|
||||
|
||||
randomValue = randomValue & mask;
|
||||
|
||||
} while (randomValue >= range);
|
||||
|
||||
return min + randomValue;
|
||||
}
|
||||
|
||||
getSafeRandomFloat(minFloat, maxFloat, steps = 1000) {
|
||||
if (typeof minFloat !== 'number' || typeof maxFloat !== 'number') {
|
||||
throw new Error('getSafeRandomFloat requires numeric min and max');
|
||||
}
|
||||
if (minFloat >= maxFloat) {
|
||||
throw new Error('minFloat must be less than maxFloat');
|
||||
}
|
||||
const randomIndex = this.getSafeRandomInt(0, steps);
|
||||
|
||||
const step = (maxFloat - minFloat) / steps;
|
||||
|
||||
return minFloat + (randomIndex * step);
|
||||
}
|
||||
|
||||
generateFingerprintMask() {
|
||||
const mask = {
|
||||
timingOffset: this.getSafeRandomInt(0, 1500),
|
||||
sizeVariation: this.getSafeRandomFloat(0.75, 1.25, 1000),
|
||||
noisePattern: Array.from(crypto.getRandomValues(new Uint8Array(64))),
|
||||
headerVariations: [
|
||||
'X-Client-Version', 'X-Session-ID', 'X-Request-ID', 'X-Timestamp', 'X-Signature',
|
||||
'X-Secure', 'X-Encrypted', 'X-Protected', 'X-Safe', 'X-Anonymous', 'X-Private'
|
||||
],
|
||||
noiseIntensity: this.getSafeRandomInt(50, 150),
|
||||
sizeMultiplier: this.getSafeRandomFloat(0.75, 1.25, 1000),
|
||||
timingVariation: this.getSafeRandomInt(100, 1100)
|
||||
};
|
||||
return mask;
|
||||
}
|
||||
|
||||
// Security configuration - all features enabled by default
|
||||
configureSecurityForSession(sessionType, securityLevel) {
|
||||
this._secureLog('info', `🔧 Configuring security for ${sessionType} session (${securityLevel} level)`);
|
||||
|
||||
this.currentSessionType = sessionType;
|
||||
this.currentSecurityLevel = securityLevel;
|
||||
configureSecurityForSession() {
|
||||
this._secureLog('info', '🔧 Configuring security - all features enabled by default');
|
||||
|
||||
// All security features are enabled by default - no payment required
|
||||
this.sessionConstraints = {};
|
||||
@@ -4499,7 +4605,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
this.applySessionConstraints();
|
||||
|
||||
this._secureLog('info', `✅ Security configured for ${sessionType} - all features enabled`, { constraints: this.sessionConstraints });
|
||||
this._secureLog('info', '✅ Security configured - all features enabled', { constraints: this.sessionConstraints });
|
||||
|
||||
if (!this._validateCryptographicSecurity()) {
|
||||
this._secureLog('error', '🚨 CRITICAL: Cryptographic security validation failed after session configuration');
|
||||
@@ -4507,7 +4613,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
if (this.onStatusChange) {
|
||||
this.onStatusChange('security_breach', {
|
||||
type: 'crypto_security_failure',
|
||||
sessionType: sessionType,
|
||||
message: 'Cryptographic security validation failed after session configuration'
|
||||
});
|
||||
}
|
||||
@@ -4644,27 +4749,21 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
// Security Level Notification
|
||||
notifySecurityLevel() {
|
||||
// Avoid duplicate notifications for the same security level
|
||||
if (this.lastSecurityLevelNotification === this.currentSecurityLevel) {
|
||||
// Avoid duplicate notifications
|
||||
if (this.lastSecurityLevelNotification === 'maximum') {
|
||||
return; // prevent duplication
|
||||
}
|
||||
|
||||
this.lastSecurityLevelNotification = this.currentSecurityLevel;
|
||||
this.lastSecurityLevelNotification = 'maximum';
|
||||
|
||||
const levelMessages = {
|
||||
'basic': '🔒 Basic Security Active - Demo session with essential protection',
|
||||
'enhanced': '🔐 Enhanced Security Active - Paid session with advanced protection',
|
||||
'maximum': '🛡️ Maximum Security Active - Premium session with complete protection'
|
||||
};
|
||||
|
||||
const message = levelMessages[this.currentSecurityLevel] || levelMessages['basic'];
|
||||
const message = '🛡️ Maximum Security Active - All features enabled';
|
||||
|
||||
if (this.onMessage) {
|
||||
this.deliverMessageToUI(message, 'system');
|
||||
}
|
||||
|
||||
// Showing details of functions for paid sessions
|
||||
if (this.currentSecurityLevel !== 'basic' && this.onMessage) {
|
||||
// Showing details of active features
|
||||
if (this.onMessage) {
|
||||
const activeFeatures = Object.entries(this.securityFeatures)
|
||||
.filter(([key, value]) => value === true)
|
||||
.map(([key]) => key.replace('has', '').replace(/([A-Z])/g, ' $1').trim().toLowerCase())
|
||||
@@ -4741,7 +4840,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
result.set(new Uint8Array(encrypted), EnhancedSecureWebRTCManager.SIZES.NESTED_ENCRYPTION_IV_SIZE);
|
||||
|
||||
this._secureLog('debug', '✅ Nested encryption applied with secure IV', {
|
||||
ivHash: await this._createSafeLogHash(uniqueIV, 'nestedEncryption'),
|
||||
ivSize: uniqueIV.length,
|
||||
dataSize: data.byteLength,
|
||||
encryptedSize: encrypted.byteLength
|
||||
@@ -4926,8 +5024,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
|
||||
// FIX: Increase intervals to reduce load
|
||||
const nextInterval = this.fakeTrafficConfig.randomDecoyIntervals ?
|
||||
Math.random() * (this.fakeTrafficConfig.maxInterval - this.fakeTrafficConfig.minInterval) +
|
||||
this.fakeTrafficConfig.minInterval :
|
||||
this.getUnbiasedRandomInRange(this.fakeTrafficConfig.minInterval, Math.min(this.fakeTrafficConfig.maxInterval, 60000)) : // Cap at 60 seconds
|
||||
this.fakeTrafficConfig.minInterval;
|
||||
|
||||
// Minimum interval 15 seconds for stability
|
||||
@@ -4943,7 +5040,10 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
};
|
||||
|
||||
// Start fake traffic generation with longer initial delay
|
||||
const initialDelay = Math.random() * this.fakeTrafficConfig.maxInterval + EnhancedSecureWebRTCManager.TIMEOUTS.DECOY_INITIAL_DELAY; // Add 5 seconds minimum
|
||||
// Use a reasonable range for initial delay (5-30 seconds)
|
||||
const minDelay = EnhancedSecureWebRTCManager.TIMEOUTS.DECOY_INITIAL_DELAY;
|
||||
const maxDelay = Math.min(this.fakeTrafficConfig.maxInterval, 30000); // Cap at 30 seconds
|
||||
const initialDelay = this.getUnbiasedRandomInRange(minDelay, maxDelay);
|
||||
this.fakeTrafficTimer = setTimeout(sendFakeMessage, initialDelay);
|
||||
}
|
||||
|
||||
@@ -4955,13 +5055,10 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
||||
}
|
||||
|
||||
generateFakeMessage() {
|
||||
const pattern = this.fakeTrafficConfig.patterns[
|
||||
Math.floor(Math.random() * this.fakeTrafficConfig.patterns.length)
|
||||
];
|
||||
const patternIndex = this.getUnbiasedRandomInRange(0, this.fakeTrafficConfig.patterns.length - 1);
|
||||
const pattern = this.fakeTrafficConfig.patterns[patternIndex];
|
||||
|
||||
const size = Math.floor(Math.random() *
|
||||
(this.fakeTrafficConfig.maxSize - this.fakeTrafficConfig.minSize + 1)) +
|
||||
this.fakeTrafficConfig.minSize;
|
||||
const size = this.getUnbiasedRandomInRange(this.fakeTrafficConfig.minSize, this.fakeTrafficConfig.maxSize);
|
||||
|
||||
const fakeData = crypto.getRandomValues(new Uint8Array(size));
|
||||
|
||||
@@ -5562,7 +5659,7 @@ async processOrderedPackets() {
|
||||
|
||||
addNoise(data) {
|
||||
const dataArray = new Uint8Array(data);
|
||||
const noiseSize = Math.floor(Math.random() * 32) + 8; // 8-40 bytes
|
||||
const noiseSize = this.getUnbiasedRandomInRange(8, 40); // 8-40 bytes
|
||||
const noise = crypto.getRandomValues(new Uint8Array(noiseSize));
|
||||
|
||||
const result = new Uint8Array(dataArray.length + noiseSize);
|
||||
@@ -5607,12 +5704,12 @@ async processOrderedPackets() {
|
||||
|
||||
addRandomHeaders(data) {
|
||||
const dataArray = new Uint8Array(data);
|
||||
const headerCount = Math.floor(Math.random() * 3) + 1; // 1-3 headers
|
||||
const headerCount = this.getUnbiasedRandomInRange(1, 3); // 1-3 headers
|
||||
let totalHeaderSize = 0;
|
||||
|
||||
// Calculate total header size
|
||||
for (let i = 0; i < headerCount; i++) {
|
||||
totalHeaderSize += 4 + Math.floor(Math.random() * 16) + 4; // size + data + checksum
|
||||
totalHeaderSize += 4 + this.getUnbiasedRandomInRange(0, 15) + 4; // size + data + checksum
|
||||
}
|
||||
|
||||
const result = new Uint8Array(totalHeaderSize + dataArray.length);
|
||||
@@ -5620,10 +5717,21 @@ async processOrderedPackets() {
|
||||
|
||||
// Add random headers
|
||||
for (let i = 0; i < headerCount; i++) {
|
||||
const headerName = this.fingerprintMask.headerVariations[
|
||||
Math.floor(Math.random() * this.fingerprintMask.headerVariations.length)
|
||||
];
|
||||
const headerData = crypto.getRandomValues(new Uint8Array(Math.floor(Math.random() * 16) + 4));
|
||||
// Generate unbiased random index for header selection
|
||||
let headerIndex;
|
||||
do {
|
||||
headerIndex = crypto.getRandomValues(new Uint8Array(1))[0];
|
||||
} while (headerIndex >= 256 - (256 % this.fingerprintMask.headerVariations.length));
|
||||
|
||||
const headerName = this.fingerprintMask.headerVariations[headerIndex % this.fingerprintMask.headerVariations.length];
|
||||
|
||||
// Generate unbiased random size for header data (4-19 bytes)
|
||||
let headerSize;
|
||||
do {
|
||||
headerSize = crypto.getRandomValues(new Uint8Array(1))[0];
|
||||
} while (headerSize >= 256 - (256 % 16));
|
||||
|
||||
const headerData = crypto.getRandomValues(new Uint8Array((headerSize % 16) + 4));
|
||||
|
||||
// Header structure: [size:4][name:4][data:variable][checksum:4]
|
||||
const headerView = new DataView(result.buffer, offset);
|
||||
@@ -6003,6 +6111,11 @@ async processOrderedPackets() {
|
||||
}
|
||||
|
||||
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', {
|
||||
hasDataChannel: !!this.dataChannel,
|
||||
dataChannelReady: this.dataChannel?.readyState === 'open',
|
||||
@@ -6520,11 +6633,10 @@ async processMessage(data) {
|
||||
if (this.sessionConstraints?.hasAntiFingerprinting) {
|
||||
this.securityFeatures.hasAntiFingerprinting = true;
|
||||
this.antiFingerprintingConfig.enabled = true;
|
||||
if (this.currentSecurityLevel === 'enhanced') {
|
||||
this.antiFingerprintingConfig.randomizeSizes = false;
|
||||
this.antiFingerprintingConfig.maskPatterns = false;
|
||||
this.antiFingerprintingConfig.useRandomHeaders = false;
|
||||
}
|
||||
// Enable full anti-fingerprinting features
|
||||
this.antiFingerprintingConfig.randomizeSizes = true;
|
||||
this.antiFingerprintingConfig.maskPatterns = true;
|
||||
this.antiFingerprintingConfig.useRandomHeaders = true;
|
||||
}
|
||||
|
||||
this.notifySecurityUpgrade(2);
|
||||
@@ -6535,10 +6647,7 @@ async processMessage(data) {
|
||||
|
||||
// Method to enable Stage 3 features (traffic obfuscation)
|
||||
enableStage3Security() {
|
||||
if (this.currentSecurityLevel !== 'maximum') {
|
||||
this._secureLog('info', '🔒 Stage 3 features only available for premium sessions');
|
||||
return;
|
||||
}
|
||||
this._secureLog('info', '🔒 Enabling Stage 3 features (traffic obfuscation)');
|
||||
|
||||
if (this.sessionConstraints?.hasMessageChunking) {
|
||||
this.securityFeatures.hasMessageChunking = true;
|
||||
@@ -6559,10 +6668,7 @@ async processMessage(data) {
|
||||
|
||||
// Method for enabling Stage 4 functions (maximum safety)
|
||||
enableStage4Security() {
|
||||
if (this.currentSecurityLevel !== 'maximum') {
|
||||
this._secureLog('info', '🔒 Stage 4 features only available for premium sessions');
|
||||
return;
|
||||
}
|
||||
this._secureLog('info', '🔒 Enabling Stage 4 features (maximum safety)');
|
||||
|
||||
if (this.sessionConstraints?.hasDecoyChannels && this.isConnected() && this.isVerified) {
|
||||
this.securityFeatures.hasDecoyChannels = true;
|
||||
@@ -6603,14 +6709,11 @@ async processMessage(data) {
|
||||
.filter(([key, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
|
||||
const stage = this.currentSecurityLevel === 'basic' ? 1 :
|
||||
this.currentSecurityLevel === 'enhanced' ? 2 :
|
||||
this.currentSecurityLevel === 'maximum' ? 4 : 1;
|
||||
const stage = 4; // Maximum security stage
|
||||
|
||||
return {
|
||||
stage: stage,
|
||||
sessionType: this.currentSessionType,
|
||||
securityLevel: this.currentSecurityLevel,
|
||||
securityLevel: 'maximum',
|
||||
activeFeatures: activeFeatures,
|
||||
totalFeatures: Object.keys(this.securityFeatures).length,
|
||||
activeFeaturesCount: activeFeatures.length,
|
||||
@@ -6730,12 +6833,7 @@ async processMessage(data) {
|
||||
|
||||
// Method for automatic feature enablement with stability check
|
||||
async autoEnableSecurityFeatures() {
|
||||
if (this.currentSessionType === 'demo') {
|
||||
this._secureLog('info', 'Demo session - keeping basic security only');
|
||||
await this.calculateAndReportSecurityLevel();
|
||||
this.notifySecurityUpgrade(1);
|
||||
return;
|
||||
}
|
||||
this._secureLog('info', 'Starting graduated security activation - all features enabled');
|
||||
|
||||
const checkStability = () => {
|
||||
const isStable = this.isConnected() &&
|
||||
@@ -6746,18 +6844,15 @@ async processMessage(data) {
|
||||
return isStable;
|
||||
};
|
||||
|
||||
this._secureLog('info', ` ${this.currentSessionType} session - starting graduated security activation`);
|
||||
await this.calculateAndReportSecurityLevel();
|
||||
this.notifySecurityUpgrade(1);
|
||||
|
||||
if (this.currentSecurityLevel === 'enhanced' || this.currentSecurityLevel === 'maximum') {
|
||||
// Enable all security stages progressively
|
||||
setTimeout(async () => {
|
||||
if (checkStability()) {
|
||||
this.enableStage2Security();
|
||||
await this.calculateAndReportSecurityLevel();
|
||||
|
||||
// For maximum sessions, turn on Stage 3 and 4
|
||||
if (this.currentSecurityLevel === 'maximum') {
|
||||
setTimeout(async () => {
|
||||
if (checkStability()) {
|
||||
this.enableStage3Security();
|
||||
@@ -6771,10 +6866,8 @@ async processMessage(data) {
|
||||
}, 20000);
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -7981,8 +8074,6 @@ async processMessage(data) {
|
||||
|
||||
this._secureLog('debug', 'Ephemeral ECDH keys generated and validated for PFS', {
|
||||
operationId: operationId,
|
||||
privateKeyHash: await this._createSafeLogHash(ecdhKeyPair.privateKey, 'ecdh_private'),
|
||||
publicKeyHash: await this._createSafeLogHash(ecdhKeyPair.publicKey, 'ecdh_public'),
|
||||
privateKeyType: ecdhKeyPair.privateKey.algorithm?.name,
|
||||
publicKeyType: ecdhKeyPair.publicKey.algorithm?.name,
|
||||
isEphemeral: true
|
||||
@@ -8012,8 +8103,6 @@ async processMessage(data) {
|
||||
|
||||
this._secureLog('debug', 'ECDSA keys generated and validated', {
|
||||
operationId: operationId,
|
||||
privateKeyHash: await this._createSafeLogHash(ecdsaKeyPair.privateKey, 'ecdsa_private'),
|
||||
publicKeyHash: await this._createSafeLogHash(ecdsaKeyPair.publicKey, 'ecdsa_public'),
|
||||
privateKeyType: ecdsaKeyPair.privateKey.algorithm?.name,
|
||||
publicKeyType: ecdsaKeyPair.publicKey.algorithm?.name
|
||||
});
|
||||
@@ -9581,9 +9670,9 @@ async processMessage(data) {
|
||||
throw new Error('Missing required security fields in offer data – possible MITM attack');
|
||||
}
|
||||
|
||||
// Replay attack protection (window reduced to 5 minutes)
|
||||
// Replay attack protection (extended to 30 minutes for better UX)
|
||||
const offerAge = Date.now() - timestamp;
|
||||
const MAX_OFFER_AGE = 300000; // 5 minutes instead of 1 hour
|
||||
const MAX_OFFER_AGE = 1800000; // 30 minutes for better user experience
|
||||
|
||||
if (offerAge > MAX_OFFER_AGE) {
|
||||
this._secureLog('error', 'Offer data is too old - possible replay attack', {
|
||||
@@ -9728,22 +9817,47 @@ async processMessage(data) {
|
||||
let derivedKeys;
|
||||
|
||||
try {
|
||||
this._secureLog('debug', 'About to call deriveSharedKeys', {
|
||||
operationId: operationId,
|
||||
privateKeyType: typeof this.ecdhKeyPair.privateKey,
|
||||
publicKeyType: typeof peerECDHPublicKey,
|
||||
saltLength: this.sessionSalt?.length,
|
||||
privateKeyAlgorithm: this.ecdhKeyPair.privateKey?.algorithm?.name,
|
||||
publicKeyAlgorithm: peerECDHPublicKey?.algorithm?.name
|
||||
});
|
||||
|
||||
derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
|
||||
this.ecdhKeyPair.privateKey,
|
||||
peerECDHPublicKey,
|
||||
this.sessionSalt
|
||||
);
|
||||
|
||||
this._secureLog('debug', 'deriveSharedKeys completed successfully', {
|
||||
operationId: operationId,
|
||||
hasMessageKey: !!derivedKeys.messageKey,
|
||||
hasMacKey: !!derivedKeys.macKey,
|
||||
hasPfsKey: !!derivedKeys.pfsKey,
|
||||
hasMetadataKey: !!derivedKeys.metadataKey,
|
||||
hasFingerprint: !!derivedKeys.fingerprint
|
||||
});
|
||||
} catch (error) {
|
||||
this._secureLog('error', 'Failed to derive shared keys', {
|
||||
operationId: operationId,
|
||||
errorType: error.constructor.name
|
||||
errorType: error.constructor.name,
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
privateKeyType: typeof this.ecdhKeyPair.privateKey,
|
||||
publicKeyType: typeof peerECDHPublicKey,
|
||||
saltLength: this.sessionSalt?.length,
|
||||
privateKeyAlgorithm: this.ecdhKeyPair.privateKey?.algorithm?.name,
|
||||
publicKeyAlgorithm: peerECDHPublicKey?.algorithm?.name
|
||||
});
|
||||
this._throwSecureError(error, 'key_derivation');
|
||||
}
|
||||
|
||||
// Securely set keys via helper
|
||||
await this._setEncryptionKeys(
|
||||
derivedKeys.encryptionKey,
|
||||
derivedKeys.messageKey,
|
||||
derivedKeys.macKey,
|
||||
derivedKeys.metadataKey,
|
||||
derivedKeys.fingerprint
|
||||
@@ -10388,10 +10502,7 @@ async processMessage(data) {
|
||||
|
||||
// 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', {
|
||||
expectedSessionIdHash: await this._createSafeLogHash(this.sessionId, 'session_id'),
|
||||
receivedSessionIdHash: await this._createSafeLogHash(answerData.sessionId, 'session_id')
|
||||
});
|
||||
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Session ID mismatch detected - possible MITM attack', {});
|
||||
throw new Error('Session ID mismatch – possible MITM attack');
|
||||
}
|
||||
|
||||
@@ -10486,7 +10597,7 @@ async processMessage(data) {
|
||||
this.sessionSalt
|
||||
);
|
||||
|
||||
this.encryptionKey = derivedKeys.encryptionKey;
|
||||
this.encryptionKey = derivedKeys.messageKey;
|
||||
this.macKey = derivedKeys.macKey;
|
||||
this.metadataKey = derivedKeys.metadataKey;
|
||||
this.keyFingerprint = derivedKeys.fingerprint;
|
||||
@@ -11116,6 +11227,10 @@ async processMessage(data) {
|
||||
throw new Error('Connection lost during message preparation');
|
||||
}
|
||||
|
||||
// Note: master key session is managed by SecureMasterKeyManager
|
||||
// Do not gate here on _isUnlocked to avoid false blocking
|
||||
// Session timers are handled inside the master key manager on key access
|
||||
|
||||
// Validate keys inside critical section
|
||||
if (!this.encryptionKey || !this.macKey || !this.metadataKey) {
|
||||
throw new Error('Encryption keys not initialized');
|
||||
@@ -11172,7 +11287,19 @@ async processMessage(data) {
|
||||
operationId: operationId,
|
||||
errorType: error.constructor.name
|
||||
});
|
||||
throw error;
|
||||
|
||||
// Improved user-facing error messages (English)
|
||||
if (error.message.includes('Session expired')) {
|
||||
throw new Error('Session expired. Please enter your password to unlock.');
|
||||
} else if (error.message.includes('Encryption keys not initialized')) {
|
||||
throw new Error('Session expired due to inactivity. Please reconnect to the chat.');
|
||||
} else if (error.message.includes('Connection lost')) {
|
||||
throw new Error('Connection lost. Please check your Internet connection.');
|
||||
} else if (error.message.includes('Rate limit exceeded')) {
|
||||
throw new Error('Message rate limit exceeded. Please wait before sending another message.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, 2000); // Reduced timeout for crypto operations
|
||||
}
|
||||
@@ -11528,7 +11655,7 @@ async processMessage(data) {
|
||||
if (error.message.includes('Connection not ready')) {
|
||||
throw new Error('Connection not ready for file transfer. Check connection status.');
|
||||
} else if (error.message.includes('Encryption keys not initialized')) {
|
||||
throw new Error('Encryption keys not initialized. Try reconnecting.');
|
||||
throw new Error('Session expired due to inactivity. Please reconnect to the chat.');
|
||||
} else if (error.message.includes('Transfer timeout')) {
|
||||
throw new Error('File transfer timeout. Check connection and try again.');
|
||||
} else {
|
||||
@@ -11644,11 +11771,10 @@ async processMessage(data) {
|
||||
|
||||
// Update session state
|
||||
this.currentSession = sessionData;
|
||||
this.sessionManager = sessionData.sessionManager;
|
||||
|
||||
// FIX: More lenient checks for activation
|
||||
const hasKeys = !!(this.encryptionKey && this.macKey);
|
||||
const hasSession = !!(this.sessionManager && (this.sessionManager.hasActiveSession?.() || sessionData.sessionId));
|
||||
const hasSession = !!(sessionData.sessionId);
|
||||
|
||||
// Force connection status if there is an active session
|
||||
if (hasSession) {
|
||||
@@ -12028,11 +12154,10 @@ class SecureKeyStorage {
|
||||
/**
|
||||
* Get master key (with automatic unlock if needed)
|
||||
*/
|
||||
async _getMasterKey() {
|
||||
async _ensureMasterKeyUnlocked() {
|
||||
if (!this._masterKeyManager.isUnlocked()) {
|
||||
await this._masterKeyManager.unlock();
|
||||
}
|
||||
return this._masterKeyManager.getMasterKey();
|
||||
}
|
||||
|
||||
async storeKey(keyId, cryptoKey, metadata = {}) {
|
||||
@@ -12111,7 +12236,6 @@ class SecureKeyStorage {
|
||||
|
||||
} catch (error) {
|
||||
this._secureLog('error', 'Failed to retrieve key', {
|
||||
keyIdHash: await this._createSafeLogHash(keyId, 'key_id'),
|
||||
errorType: error?.constructor?.name || 'Unknown'
|
||||
});
|
||||
return null;
|
||||
@@ -12126,20 +12250,12 @@ class SecureKeyStorage {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(dataToEncrypt);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
const masterKey = await this._getMasterKey();
|
||||
const encryptedData = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
masterKey,
|
||||
data
|
||||
);
|
||||
|
||||
await this._ensureMasterKeyUnlocked();
|
||||
const { encryptedData, iv } = await this._masterKeyManager.encryptBytes(data);
|
||||
// Return IV + encrypted data
|
||||
const result = new Uint8Array(iv.length + encryptedData.byteLength);
|
||||
result.set(iv, 0);
|
||||
result.set(new Uint8Array(encryptedData), iv.length);
|
||||
|
||||
result.set(encryptedData, iv.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -12147,12 +12263,8 @@ class SecureKeyStorage {
|
||||
const iv = encryptedData.slice(0, 12);
|
||||
const data = encryptedData.slice(12);
|
||||
|
||||
const masterKey = await this._getMasterKey();
|
||||
const decryptedData = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
masterKey,
|
||||
data
|
||||
);
|
||||
await this._ensureMasterKeyUnlocked();
|
||||
const decryptedData = await this._masterKeyManager.decryptBytes(data, iv);
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const jsonString = decoder.decode(decryptedData);
|
||||
@@ -12305,7 +12417,6 @@ class SecureKeyStorage {
|
||||
|
||||
} catch (error) {
|
||||
this._secureLog('error', 'Failed to delete key', {
|
||||
keyIdHash: await this._createSafeLogHash(keyId, 'key_id'),
|
||||
errorType: error?.constructor?.name || 'Unknown'
|
||||
});
|
||||
return false;
|
||||
@@ -12508,7 +12619,7 @@ class SecureKeyStorage {
|
||||
// Additional info
|
||||
connectionId: this.connectionId,
|
||||
keyFingerprint: this.keyFingerprint,
|
||||
currentSecurityLevel: this.currentSecurityLevel,
|
||||
currentSecurityLevel: 'maximum',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
@@ -12842,11 +12953,8 @@ class SecurePersistentKeyStorage {
|
||||
// Export key to JWK
|
||||
const jwkData = await crypto.subtle.exportKey('jwk', cryptoKey);
|
||||
|
||||
// Get master key for encryption
|
||||
const masterKey = this._masterKeyManager.getMasterKey();
|
||||
|
||||
// Encrypt JWK data
|
||||
const { encryptedData, iv } = await this._encryptKeyData(jwkData, masterKey);
|
||||
const { encryptedData, iv } = await this._encryptKeyData(jwkData);
|
||||
|
||||
// Store encrypted data in IndexedDB
|
||||
await this._indexedDB.storeEncryptedKey(
|
||||
@@ -12888,11 +12996,8 @@ class SecurePersistentKeyStorage {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get master key for decryption
|
||||
const masterKey = this._masterKeyManager.getMasterKey();
|
||||
|
||||
// Decrypt JWK data
|
||||
const jwkData = await this._decryptKeyData(keyRecord.encryptedData, keyRecord.iv, masterKey);
|
||||
const jwkData = await this._decryptKeyData(keyRecord.encryptedData, keyRecord.iv);
|
||||
|
||||
// Import as non-extractable key
|
||||
const restoredKey = await this._importAsNonExtractable(jwkData, keyRecord.algorithm, keyRecord.usages);
|
||||
@@ -12965,37 +13070,21 @@ class SecurePersistentKeyStorage {
|
||||
/**
|
||||
* Encrypt key data using master key
|
||||
*/
|
||||
async _encryptKeyData(jwkData, masterKey) {
|
||||
async _encryptKeyData(jwkData) {
|
||||
// Convert JWK to JSON string and then to bytes
|
||||
const jsonString = JSON.stringify(jwkData);
|
||||
const data = new TextEncoder().encode(jsonString);
|
||||
|
||||
// Generate random IV
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Encrypt with AES-GCM
|
||||
const encryptedData = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
masterKey,
|
||||
data
|
||||
);
|
||||
|
||||
return {
|
||||
encryptedData: new Uint8Array(encryptedData),
|
||||
iv: iv
|
||||
};
|
||||
await this._ensureMasterKeyUnlocked();
|
||||
return await this._masterKeyManager.encryptBytes(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt key data using master key
|
||||
*/
|
||||
async _decryptKeyData(encryptedData, iv, masterKey) {
|
||||
// Decrypt with AES-GCM
|
||||
const decryptedData = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
masterKey,
|
||||
encryptedData
|
||||
);
|
||||
async _decryptKeyData(encryptedData, iv) {
|
||||
await this._ensureMasterKeyUnlocked();
|
||||
const decryptedData = await this._masterKeyManager.decryptBytes(encryptedData, iv);
|
||||
|
||||
// Convert back to JWK
|
||||
const jsonString = new TextDecoder().decode(decryptedData);
|
||||
@@ -13050,17 +13139,17 @@ class SecurePersistentKeyStorage {
|
||||
class SecureMasterKeyManager {
|
||||
constructor(indexedDBWrapper = null) {
|
||||
// Session state
|
||||
this._masterKey = null;
|
||||
this._keyHandle = null; // non-extractable CryptoKey
|
||||
this._isUnlocked = false;
|
||||
this._sessionTimeout = null;
|
||||
this._lastActivity = null;
|
||||
|
||||
// Configuration
|
||||
this._sessionTimeoutMs = 15 * 60 * 1000; // 15 minutes
|
||||
this._inactivityTimeoutMs = 5 * 60 * 1000; // 5 minutes
|
||||
this._sessionTimeoutMs = 60 * 60 * 1000; // 60 minutes (увеличено с 15 минут)
|
||||
this._inactivityTimeoutMs = 30 * 60 * 1000; // 30 minutes (увеличено с 5 минут)
|
||||
|
||||
// PBKDF2 parameters
|
||||
this._pbkdf2Iterations = 100000; // 100k iterations
|
||||
this._pbkdf2Iterations = 310000; // OWASP 2025 recommendation for PBKDF2-SHA256
|
||||
this._saltSize = 32; // 256 bits
|
||||
|
||||
// IndexedDB wrapper for persistent salt storage
|
||||
@@ -13072,8 +13161,8 @@ class SecureMasterKeyManager {
|
||||
this._onSessionExpired = null;
|
||||
this._onUnlocked = null;
|
||||
|
||||
// Setup event listeners
|
||||
this._setupEventListeners();
|
||||
// Setup event listeners (disabled for better UX - no auto-disconnect)
|
||||
// this._setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13308,8 +13397,8 @@ class SecureMasterKeyManager {
|
||||
// Get or create persistent salt
|
||||
const salt = await this._getOrCreateSalt();
|
||||
|
||||
// Derive master key
|
||||
this._masterKey = await this._deriveKeyFromPassword(password, salt);
|
||||
// Derive non-extractable key handle
|
||||
this._keyHandle = await this._deriveKeyFromPassword(password, salt);
|
||||
|
||||
// Mark as unlocked
|
||||
this._isUnlocked = true;
|
||||
@@ -13344,13 +13433,24 @@ class SecureMasterKeyManager {
|
||||
/**
|
||||
* Get master key (only if unlocked)
|
||||
*/
|
||||
getMasterKey() {
|
||||
if (!this._isUnlocked || !this._masterKey) {
|
||||
// Prevent direct key access; provide operations only
|
||||
async encryptBytes(plainBytes) {
|
||||
if (!this._isUnlocked || !this._keyHandle) {
|
||||
throw new Error('Master key is locked');
|
||||
}
|
||||
|
||||
this._updateActivity();
|
||||
return this._masterKey;
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, this._keyHandle, plainBytes);
|
||||
return { encryptedData: new Uint8Array(encrypted), iv };
|
||||
}
|
||||
|
||||
async decryptBytes(encryptedBytes, iv) {
|
||||
if (!this._isUnlocked || !this._keyHandle) {
|
||||
throw new Error('Master key is locked');
|
||||
}
|
||||
this._updateActivity();
|
||||
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, this._keyHandle, encryptedBytes);
|
||||
return new Uint8Array(decrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13376,10 +13476,10 @@ class SecureMasterKeyManager {
|
||||
* Securely wipe master key from memory
|
||||
*/
|
||||
_secureWipeMasterKey() {
|
||||
if (this._masterKey) {
|
||||
if (this._keyHandle) {
|
||||
// CryptoKey objects are automatically garbage collected
|
||||
// but we clear the reference immediately
|
||||
this._masterKey = null;
|
||||
this._keyHandle = null;
|
||||
}
|
||||
this._clearTimers();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Notification Integration Module for SecureBit WebRTC Chat
|
||||
* Integrates secure notifications with existing WebRTC architecture
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author SecureBit Team
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { SecureChatNotificationManager } from './SecureNotificationManager.js';
|
||||
|
||||
class NotificationIntegration {
|
||||
constructor(webrtcManager) {
|
||||
this.webrtcManager = webrtcManager;
|
||||
this.notificationManager = new SecureChatNotificationManager({
|
||||
maxQueueSize: 10,
|
||||
rateLimitMs: 1000, // Reduced from 2000ms to 1000ms
|
||||
trustedOrigins: [
|
||||
window.location.origin,
|
||||
// Add other trusted origins for CDN icons
|
||||
]
|
||||
});
|
||||
|
||||
this.isInitialized = false;
|
||||
this.originalOnMessage = null;
|
||||
this.originalOnStatusChange = null;
|
||||
this.processedMessages = new Set(); // Track processed messages to avoid duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize notification integration
|
||||
* @returns {Promise<boolean>} Initialization success
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
if (this.isInitialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Store original callbacks
|
||||
this.originalOnMessage = this.webrtcManager.onMessage;
|
||||
this.originalOnStatusChange = this.webrtcManager.onStatusChange;
|
||||
|
||||
|
||||
// Wrap the original onMessage callback
|
||||
this.webrtcManager.onMessage = (message, type) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
|
||||
// Call original callback if it exists
|
||||
if (this.originalOnMessage) {
|
||||
this.originalOnMessage(message, type);
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap the original onStatusChange callback
|
||||
this.webrtcManager.onStatusChange = (status) => {
|
||||
this.handleStatusChange(status);
|
||||
|
||||
// Call original callback if it exists
|
||||
if (this.originalOnStatusChange) {
|
||||
this.originalOnStatusChange(status);
|
||||
}
|
||||
};
|
||||
|
||||
// Also hook into the deliverMessageToUI method if it exists
|
||||
if (this.webrtcManager.deliverMessageToUI) {
|
||||
this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);
|
||||
this.webrtcManager.deliverMessageToUI = (message, type) => {
|
||||
this.handleIncomingMessage(message, type);
|
||||
this.originalDeliverMessageToUI(message, type);
|
||||
};
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages and trigger notifications
|
||||
* @param {*} message - Message content
|
||||
* @param {string} type - Message type
|
||||
* @private
|
||||
*/
|
||||
handleIncomingMessage(message, type) {
|
||||
try {
|
||||
// Create a unique key for this message to avoid duplicates
|
||||
const messageKey = `${type}:${typeof message === 'string' ? message : JSON.stringify(message)}`;
|
||||
|
||||
// Skip if we've already processed this message
|
||||
if (this.processedMessages.has(messageKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark message as processed
|
||||
this.processedMessages.add(messageKey);
|
||||
|
||||
// Clean up old processed messages (keep only last 100)
|
||||
if (this.processedMessages.size > 100) {
|
||||
const messagesArray = Array.from(this.processedMessages);
|
||||
this.processedMessages.clear();
|
||||
messagesArray.slice(-50).forEach(msg => this.processedMessages.add(msg));
|
||||
}
|
||||
|
||||
|
||||
// Only process chat messages, not system messages
|
||||
if (type === 'system' || type === 'file-transfer' || type === 'heartbeat') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract message information
|
||||
const messageInfo = this.extractMessageInfo(message, type);
|
||||
if (!messageInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send notification
|
||||
const notificationResult = this.notificationManager.notify(
|
||||
messageInfo.senderName,
|
||||
messageInfo.text,
|
||||
{
|
||||
icon: messageInfo.senderAvatar,
|
||||
senderId: messageInfo.senderId,
|
||||
onClick: (senderId) => {
|
||||
this.focusChatWindow();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status changes
|
||||
* @param {string} status - Connection status
|
||||
* @private
|
||||
*/
|
||||
handleStatusChange(status) {
|
||||
try {
|
||||
// Clear notifications when connection is lost
|
||||
if (status === 'disconnected' || status === 'failed') {
|
||||
this.notificationManager.clearNotificationQueue();
|
||||
this.notificationManager.resetUnreadCount();
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message information for notifications
|
||||
* @param {*} message - Message content
|
||||
* @param {string} type - Message type
|
||||
* @returns {Object|null} Extracted message info or null
|
||||
* @private
|
||||
*/
|
||||
extractMessageInfo(message, type) {
|
||||
try {
|
||||
let messageData = message;
|
||||
|
||||
// Handle different message formats
|
||||
if (typeof message === 'string') {
|
||||
try {
|
||||
messageData = JSON.parse(message);
|
||||
} catch (e) {
|
||||
// Plain text message
|
||||
return {
|
||||
senderName: 'Peer',
|
||||
text: message,
|
||||
senderId: 'peer',
|
||||
senderAvatar: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle structured message data
|
||||
if (typeof messageData === 'object' && messageData !== null) {
|
||||
return {
|
||||
senderName: messageData.senderName || messageData.name || 'Peer',
|
||||
text: messageData.text || messageData.message || messageData.content || '',
|
||||
senderId: messageData.senderId || messageData.id || 'peer',
|
||||
senderAvatar: messageData.senderAvatar || messageData.avatar || null
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus chat window when notification is clicked
|
||||
* @private
|
||||
*/
|
||||
focusChatWindow() {
|
||||
try {
|
||||
window.focus();
|
||||
|
||||
// Scroll to bottom of messages if container exists
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async requestPermission() {
|
||||
try {
|
||||
return await this.notificationManager.requestPermission();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification status
|
||||
* @returns {Object} Notification status
|
||||
*/
|
||||
getStatus() {
|
||||
return this.notificationManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clearNotifications() {
|
||||
this.notificationManager.clearNotificationQueue();
|
||||
this.notificationManager.resetUnreadCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup integration
|
||||
*/
|
||||
cleanup() {
|
||||
try {
|
||||
if (this.isInitialized) {
|
||||
// Restore original callbacks
|
||||
if (this.originalOnMessage) {
|
||||
this.webrtcManager.onMessage = this.originalOnMessage;
|
||||
}
|
||||
if (this.originalOnStatusChange) {
|
||||
this.webrtcManager.onStatusChange = this.originalOnStatusChange;
|
||||
}
|
||||
if (this.originalDeliverMessageToUI) {
|
||||
this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;
|
||||
}
|
||||
|
||||
// Clear notifications
|
||||
this.clearNotifications();
|
||||
|
||||
this.isInitialized = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { NotificationIntegration };
|
||||
}
|
||||
|
||||
// Global export for browser usage
|
||||
if (typeof window !== 'undefined') {
|
||||
window.NotificationIntegration = NotificationIntegration;
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
/**
|
||||
* Secure and Reliable Notification Manager for P2P WebRTC Chat
|
||||
* Follows best practices: OWASP, MDN, Chrome DevRel
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author SecureBit Team
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
class SecureChatNotificationManager {
|
||||
constructor(config = {}) {
|
||||
// Safely read Notification permission (iOS Safari may not define Notification)
|
||||
this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')
|
||||
? Notification.permission
|
||||
: 'denied';
|
||||
this.isTabActive = this.checkTabActive(); // Initialize with proper check
|
||||
this.unreadCount = 0;
|
||||
this.originalTitle = document.title;
|
||||
this.notificationQueue = [];
|
||||
this.maxQueueSize = config.maxQueueSize || 5;
|
||||
this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection
|
||||
this.lastNotificationTime = 0;
|
||||
this.trustedOrigins = config.trustedOrigins || [];
|
||||
|
||||
// Secure context flag
|
||||
this.isSecureContext = window.isSecureContext;
|
||||
|
||||
// Cross-browser compatibility for Page Visibility API
|
||||
this.hidden = this.getHiddenProperty();
|
||||
this.visibilityChange = this.getVisibilityChangeEvent();
|
||||
|
||||
this.initVisibilityTracking();
|
||||
this.initSecurityChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize security checks and validation
|
||||
* @private
|
||||
*/
|
||||
initSecurityChecks() {
|
||||
// Security checks are performed silently
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hidden property name for cross-browser compatibility
|
||||
* @returns {string} Hidden property name
|
||||
* @private
|
||||
*/
|
||||
getHiddenProperty() {
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
return "hidden";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
return "msHidden";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
return "webkitHidden";
|
||||
}
|
||||
return "hidden"; // fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility change event name for cross-browser compatibility
|
||||
* @returns {string} Visibility change event name
|
||||
* @private
|
||||
*/
|
||||
getVisibilityChangeEvent() {
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
return "visibilitychange";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
return "msvisibilitychange";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
return "webkitvisibilitychange";
|
||||
}
|
||||
return "visibilitychange"; // fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tab is currently active using multiple methods
|
||||
* @returns {boolean} True if tab is active
|
||||
* @private
|
||||
*/
|
||||
checkTabActive() {
|
||||
// Primary method: Page Visibility API
|
||||
if (this.hidden && typeof document[this.hidden] !== "undefined") {
|
||||
return !document[this.hidden];
|
||||
}
|
||||
|
||||
// Fallback method: document.hasFocus()
|
||||
if (typeof document.hasFocus === "function") {
|
||||
return document.hasFocus();
|
||||
}
|
||||
|
||||
// Ultimate fallback: assume active
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize page visibility tracking (Page Visibility API)
|
||||
* @private
|
||||
*/
|
||||
initVisibilityTracking() {
|
||||
// Primary method: Page Visibility API with cross-browser support
|
||||
if (typeof document.addEventListener !== "undefined" && typeof document[this.hidden] !== "undefined") {
|
||||
document.addEventListener(this.visibilityChange, () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
|
||||
if (this.isTabActive) {
|
||||
this.resetUnreadCount();
|
||||
this.clearNotificationQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback method: Window focus/blur events
|
||||
window.addEventListener('focus', () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
if (this.isTabActive) {
|
||||
this.resetUnreadCount();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
this.isTabActive = this.checkTabActive();
|
||||
});
|
||||
|
||||
// Page unload cleanup
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.clearNotificationQueue();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission (BEST PRACTICE: Only call in response to user action)
|
||||
* Never call on page load!
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async requestPermission() {
|
||||
// Secure context check
|
||||
if (!this.isSecureContext || !('Notification' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.permission === 'granted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.permission === 'denied') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.permission = await Notification.requestPermission();
|
||||
return this.permission === 'granted';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update page title with unread count
|
||||
* @private
|
||||
*/
|
||||
updateTitle() {
|
||||
if (this.unreadCount > 0) {
|
||||
document.title = `(${this.unreadCount}) ${this.originalTitle}`;
|
||||
} else {
|
||||
document.title = this.originalTitle;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS Protection: Sanitize input text
|
||||
* @param {string} text - Text to sanitize
|
||||
* @returns {string} Sanitized text
|
||||
* @private
|
||||
*/
|
||||
sanitizeText(text) {
|
||||
if (typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove HTML tags and potentially dangerous characters
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.substring(0, 500); // Length limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate icon URL (XSS protection)
|
||||
* @param {string} url - URL to validate
|
||||
* @returns {string|null} Validated URL or null
|
||||
* @private
|
||||
*/
|
||||
validateIconUrl(url) {
|
||||
if (!url) return null;
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url, window.location.origin);
|
||||
|
||||
// Only allow HTTPS and data URLs
|
||||
if (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'data:') {
|
||||
// Check trusted origins if specified
|
||||
if (this.trustedOrigins.length > 0) {
|
||||
const isTrusted = this.trustedOrigins.some(origin =>
|
||||
parsedUrl.origin === origin
|
||||
);
|
||||
return isTrusted ? parsedUrl.href : null;
|
||||
}
|
||||
return parsedUrl.href;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting for spam protection
|
||||
* @returns {boolean} Rate limit check passed
|
||||
* @private
|
||||
*/
|
||||
checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastNotificationTime < this.rateLimitMs) {
|
||||
return false;
|
||||
}
|
||||
this.lastNotificationTime = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send secure notification
|
||||
* @param {string} senderName - Name of message sender
|
||||
* @param {string} message - Message content
|
||||
* @param {Object} options - Notification options
|
||||
* @returns {Notification|null} Created notification or null
|
||||
*/
|
||||
notify(senderName, message, options = {}) {
|
||||
// Abort if Notifications API is not available (e.g., iOS Safari)
|
||||
if (typeof Notification === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
// Update tab active state before checking
|
||||
this.isTabActive = this.checkTabActive();
|
||||
|
||||
// Only show if tab is NOT active (user is on another tab or minimized)
|
||||
if (this.isTabActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if (this.permission !== 'granted') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (!this.checkRateLimit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Data sanitization (XSS Protection)
|
||||
const safeSenderName = this.sanitizeText(senderName || 'Unknown');
|
||||
const safeMessage = this.sanitizeText(message || '');
|
||||
const safeIcon = this.validateIconUrl(options.icon) || '/logo/icon-192x192.png';
|
||||
|
||||
// Queue overflow protection
|
||||
if (this.notificationQueue.length >= this.maxQueueSize) {
|
||||
this.clearNotificationQueue();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const notification = new Notification(
|
||||
`${safeSenderName}`,
|
||||
{
|
||||
body: safeMessage.substring(0, 200), // Length limit
|
||||
icon: safeIcon,
|
||||
badge: safeIcon,
|
||||
tag: `chat-${options.senderId || 'unknown'}`, // Grouping
|
||||
requireInteraction: false, // Don't block user
|
||||
silent: options.silent || false,
|
||||
// Vibrate only for mobile and if supported
|
||||
vibrate: navigator.vibrate ? [200, 100, 200] : undefined,
|
||||
// Safe metadata
|
||||
data: {
|
||||
senderId: this.sanitizeText(options.senderId),
|
||||
timestamp: Date.now(),
|
||||
// Don't include sensitive data!
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Increment counter
|
||||
this.unreadCount++;
|
||||
this.updateTitle();
|
||||
|
||||
// Add to queue for management
|
||||
this.notificationQueue.push(notification);
|
||||
|
||||
// Safe click handler
|
||||
notification.onclick = (event) => {
|
||||
event.preventDefault(); // Prevent default behavior
|
||||
window.focus();
|
||||
notification.close();
|
||||
|
||||
// Safe callback
|
||||
if (typeof options.onClick === 'function') {
|
||||
try {
|
||||
options.onClick(options.senderId);
|
||||
} catch (error) {
|
||||
console.error('[Notifications] Error in onClick handler:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Error handler
|
||||
notification.onerror = (event) => {
|
||||
console.error('[Notifications] Error showing notification:', event);
|
||||
};
|
||||
|
||||
// Auto-close after reasonable time
|
||||
const autoCloseTimeout = Math.min(options.autoClose || 5000, 10000);
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
this.removeFromQueue(notification);
|
||||
}, autoCloseTimeout);
|
||||
|
||||
return notification;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Notifications] Failed to create notification:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove notification from queue
|
||||
* @param {Notification} notification - Notification to remove
|
||||
* @private
|
||||
*/
|
||||
removeFromQueue(notification) {
|
||||
const index = this.notificationQueue.indexOf(notification);
|
||||
if (index > -1) {
|
||||
this.notificationQueue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clearNotificationQueue() {
|
||||
this.notificationQueue.forEach(notification => {
|
||||
try {
|
||||
notification.close();
|
||||
} catch (error) {
|
||||
// Ignore errors when closing
|
||||
}
|
||||
});
|
||||
this.notificationQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset unread counter
|
||||
*/
|
||||
resetUnreadCount() {
|
||||
this.unreadCount = 0;
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status
|
||||
* @returns {Object} Current notification status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
permission: this.permission,
|
||||
isTabActive: this.isTabActive,
|
||||
unreadCount: this.unreadCount,
|
||||
isSecureContext: this.isSecureContext,
|
||||
queueSize: this.notificationQueue.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure integration with WebRTC
|
||||
*/
|
||||
class SecureP2PChat {
|
||||
constructor() {
|
||||
this.notificationManager = new SecureChatNotificationManager({
|
||||
maxQueueSize: 5,
|
||||
rateLimitMs: 2000,
|
||||
trustedOrigins: [
|
||||
window.location.origin,
|
||||
// Add other trusted origins for CDN icons
|
||||
]
|
||||
});
|
||||
|
||||
this.dataChannel = null;
|
||||
this.peerConnection = null;
|
||||
this.remotePeerName = 'Peer';
|
||||
this.messageHistory = [];
|
||||
this.maxHistorySize = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize when user connects
|
||||
*/
|
||||
async init() {
|
||||
// Initialize notification manager silently
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for manual permission request (called on click)
|
||||
* @returns {Promise<boolean>} Permission granted status
|
||||
*/
|
||||
async enableNotifications() {
|
||||
const granted = await this.notificationManager.requestPermission();
|
||||
return granted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup DataChannel with security checks
|
||||
* @param {RTCDataChannel} dataChannel - WebRTC data channel
|
||||
*/
|
||||
setupDataChannel(dataChannel) {
|
||||
if (!dataChannel) {
|
||||
console.error('[Chat] Invalid DataChannel');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataChannel = dataChannel;
|
||||
|
||||
// Setup handlers
|
||||
this.dataChannel.onmessage = (event) => {
|
||||
this.handleIncomingMessage(event.data);
|
||||
};
|
||||
|
||||
this.dataChannel.onerror = (error) => {
|
||||
// Handle error silently
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS Protection: Validate incoming messages
|
||||
* @param {string|Object} data - Message data
|
||||
* @returns {Object|null} Validated message or null
|
||||
* @private
|
||||
*/
|
||||
validateMessage(data) {
|
||||
try {
|
||||
const message = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
|
||||
// Check message structure
|
||||
if (!message || typeof message !== 'object') {
|
||||
throw new Error('Invalid message structure');
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!message.text || typeof message.text !== 'string') {
|
||||
throw new Error('Invalid message text');
|
||||
}
|
||||
|
||||
// Message length limit (DoS protection)
|
||||
if (message.text.length > 10000) {
|
||||
throw new Error('Message too long');
|
||||
}
|
||||
|
||||
return {
|
||||
text: message.text,
|
||||
senderName: message.senderName || 'Unknown',
|
||||
senderId: message.senderId || 'unknown',
|
||||
timestamp: message.timestamp || Date.now(),
|
||||
senderAvatar: message.senderAvatar || null
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Chat] Message validation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure handling of incoming messages
|
||||
* @param {string|Object} data - Message data
|
||||
* @private
|
||||
*/
|
||||
handleIncomingMessage(data) {
|
||||
const message = this.validateMessage(data);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to history (with limit)
|
||||
this.messageHistory.push(message);
|
||||
if (this.messageHistory.length > this.maxHistorySize) {
|
||||
this.messageHistory.shift();
|
||||
}
|
||||
|
||||
// Display in UI (with sanitization)
|
||||
this.displayMessage(message);
|
||||
|
||||
// Send notification only if tab is inactive
|
||||
this.notificationManager.notify(
|
||||
message.senderName,
|
||||
message.text,
|
||||
{
|
||||
icon: message.senderAvatar,
|
||||
senderId: message.senderId,
|
||||
onClick: (senderId) => {
|
||||
this.scrollToLatestMessage();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Optional: sound (with check)
|
||||
if (!this.notificationManager.isTabActive) {
|
||||
this.playNotificationSound();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS Protection: Safe message display
|
||||
* @param {Object} message - Message to display
|
||||
* @private
|
||||
*/
|
||||
displayMessage(message) {
|
||||
const container = document.getElementById('messages');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = 'message';
|
||||
|
||||
// Use textContent to prevent XSS
|
||||
const nameEl = document.createElement('strong');
|
||||
nameEl.textContent = message.senderName + ': ';
|
||||
|
||||
const textEl = document.createElement('span');
|
||||
textEl.textContent = message.text;
|
||||
textEl.style.wordWrap = 'break-word';
|
||||
textEl.style.overflowWrap = 'break-word';
|
||||
textEl.style.whiteSpace = 'normal';
|
||||
|
||||
const timeEl = document.createElement('small');
|
||||
timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();
|
||||
|
||||
messageEl.appendChild(nameEl);
|
||||
messageEl.appendChild(textEl);
|
||||
messageEl.appendChild(document.createElement('br'));
|
||||
messageEl.appendChild(timeEl);
|
||||
|
||||
container.appendChild(messageEl);
|
||||
this.scrollToLatestMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe sound playback
|
||||
* @private
|
||||
*/
|
||||
playNotificationSound() {
|
||||
try {
|
||||
// Use only local audio files
|
||||
const audio = new Audio('/assets/audio/notification.mp3');
|
||||
audio.volume = 0.3; // Moderate volume
|
||||
|
||||
// Error handling
|
||||
audio.play().catch(error => {
|
||||
// Handle audio error silently
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle audio creation error silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to latest message
|
||||
* @private
|
||||
*/
|
||||
scrollToLatestMessage() {
|
||||
const container = document.getElementById('messages');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status
|
||||
* @returns {Object} Current chat status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
notifications: this.notificationManager.getStatus(),
|
||||
messageCount: this.messageHistory.length,
|
||||
connected: this.dataChannel?.readyState === 'open'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SecureChatNotificationManager, SecureP2PChat };
|
||||
}
|
||||
|
||||
// Global export for browser usage
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SecureChatNotificationManager = SecureChatNotificationManager;
|
||||
window.SecureP2PChat = SecureP2PChat;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class PWAInstallPrompt {
|
||||
|
||||
setupEventListeners() {
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
// Don't prevent default - let browser show its own banner
|
||||
this.deferredPrompt = event;
|
||||
|
||||
if (this.checkInstallationStatus()) {
|
||||
|
||||
@@ -95,6 +95,18 @@ class PWAOfflineManager {
|
||||
|
||||
request.onsuccess = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
// Network status changes
|
||||
window.addEventListener('online', () => {
|
||||
@@ -345,12 +388,6 @@ class PWAOfflineManager {
|
||||
}
|
||||
|
||||
async queueOfflineAction(action) {
|
||||
if (!this.offlineDB) {
|
||||
console.warn('⚠️ Offline database not available');
|
||||
this.offlineQueue.push(action);
|
||||
return;
|
||||
}
|
||||
|
||||
const queueItem = {
|
||||
...action,
|
||||
id: Date.now() + Math.random(),
|
||||
@@ -360,21 +397,32 @@ class PWAOfflineManager {
|
||||
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 {
|
||||
await this.ensureDatabaseOpen();
|
||||
|
||||
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
||||
const store = transaction.objectStore('offlineQueue');
|
||||
await this.promisifyRequest(store.add(queueItem));
|
||||
|
||||
this.offlineQueue.push(queueItem);
|
||||
|
||||
// Try to register background sync
|
||||
if (this.registration) {
|
||||
await this.registration.sync.register('offline-sync');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to queue offline action:', error);
|
||||
// Fallback to memory queue
|
||||
this.offlineQueue.push(queueItem);
|
||||
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);
|
||||
}
|
||||
// Item already in memory queue, so no action needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,9 +439,32 @@ class PWAOfflineManager {
|
||||
try {
|
||||
// Process database queue
|
||||
if (this.offlineDB) {
|
||||
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
|
||||
const store = transaction.objectStore('offlineQueue');
|
||||
const allItems = await this.promisifyRequest(store.getAll());
|
||||
// Ensure database is open before processing
|
||||
await this.ensureDatabaseOpen();
|
||||
|
||||
// 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
|
||||
allItems.sort((a, b) => {
|
||||
@@ -403,10 +474,17 @@ class PWAOfflineManager {
|
||||
return a.timestamp - b.timestamp; // Older first
|
||||
});
|
||||
|
||||
// Process each item with its own transaction to avoid "database closing" errors
|
||||
for (const item of allItems) {
|
||||
try {
|
||||
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++;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to process offline action:', error);
|
||||
@@ -417,11 +495,25 @@ class PWAOfflineManager {
|
||||
|
||||
if (item.retryCount >= item.maxRetries) {
|
||||
// Max retries reached, remove from queue
|
||||
await this.promisifyRequest(store.delete(item.id));
|
||||
console.log('❌ Max retries reached for action:', item.type);
|
||||
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);
|
||||
} catch (removeError) {
|
||||
console.error('❌ Failed to remove item after max retries:', removeError);
|
||||
}
|
||||
} else {
|
||||
// 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;
|
||||
|
||||
try {
|
||||
await this.ensureDatabaseOpen();
|
||||
|
||||
const appState = {
|
||||
component: 'app_state',
|
||||
timestamp: Date.now(),
|
||||
@@ -669,7 +763,11 @@ class PWAOfflineManager {
|
||||
await this.promisifyRequest(store.put(appState));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to save application state:', 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,9 +793,20 @@ class PWAOfflineManager {
|
||||
throw new Error('Offline database not available');
|
||||
}
|
||||
|
||||
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
return this.promisifyRequest(store.put(data));
|
||||
try {
|
||||
await this.ensureDatabaseOpen();
|
||||
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
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) {
|
||||
@@ -706,11 +815,24 @@ class PWAOfflineManager {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ensureDatabaseOpen();
|
||||
const transaction = this.offlineDB.transaction([storeName], 'readonly');
|
||||
const store = transaction.objectStore(storeName);
|
||||
const result = await this.promisifyRequest(store.get(key));
|
||||
return result;
|
||||
} 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);
|
||||
return null;
|
||||
}
|
||||
@@ -720,6 +842,7 @@ class PWAOfflineManager {
|
||||
if (!this.offlineDB) return;
|
||||
|
||||
try {
|
||||
await this.ensureDatabaseOpen();
|
||||
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
@@ -730,7 +853,11 @@ class PWAOfflineManager {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to clear stored data from ${storeName}:`, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,6 +885,8 @@ class PWAOfflineManager {
|
||||
const cutoffTime = Date.now() - maxAge;
|
||||
|
||||
try {
|
||||
await this.ensureDatabaseOpen();
|
||||
|
||||
const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite');
|
||||
|
||||
// Clean offline queue
|
||||
@@ -790,7 +919,11 @@ class PWAOfflineManager {
|
||||
|
||||
console.log('🧹 Old offline data cleaned up');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to cleanup old data:', 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1030,10 +1163,25 @@ class PWAOfflineManager {
|
||||
destroy() {
|
||||
if (this.reconnectInterval) {
|
||||
clearInterval(this.reconnectInterval);
|
||||
this.reconnectInterval = null;
|
||||
}
|
||||
|
||||
// Set sync flag to prevent new operations
|
||||
this.syncInProgress = true;
|
||||
|
||||
// Close database connection
|
||||
if (this.offlineDB) {
|
||||
this.offlineDB.close();
|
||||
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 = null;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Error closing database:', error);
|
||||
this.offlineDB = null;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🧹 Offline Manager destroyed');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
|
||||
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
|
||||
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
|
||||
import { NotificationIntegration } from '../notifications/NotificationIntegration.js';
|
||||
|
||||
// Import UI components (side-effect: they attach themselves to window.*)
|
||||
import '../components/ui/SessionTimer.jsx';
|
||||
import '../components/ui/Header.jsx';
|
||||
import '../components/ui/DownloadApps.jsx';
|
||||
import '../components/ui/UniqueFeatureSlider.jsx';
|
||||
@@ -17,6 +17,7 @@ import '../components/ui/FileTransfer.jsx';
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
window.NotificationIntegration = NotificationIntegration;
|
||||
|
||||
// Mount application once DOM and modules are ready
|
||||
const start = () => {
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
// Temporary bootstrap that still uses eval for JSX components fetched as text.
|
||||
// Next step is to replace this with proper ESM imports of prebuilt JS.
|
||||
// Bootstrap that loads modules using dynamic ESM imports.
|
||||
// This approach is CSP-compliant and doesn't use eval().
|
||||
(async () => {
|
||||
try {
|
||||
const timestamp = Date.now();
|
||||
const [cryptoModule, webrtcModule, paymentModule, fileTransferModule] = await Promise.all([
|
||||
const [cryptoModule, webrtcModule, fileTransferModule, notificationModule, notificationTestModule, notificationGestureTestModule] = await Promise.all([
|
||||
import(`../crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
|
||||
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
|
||||
import(`../session/PayPerSessionManager.js?v=${timestamp}`),
|
||||
import(`../transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`),
|
||||
import(`../notifications/NotificationIntegration.js?v=${timestamp}`),
|
||||
import(`../notifications/NotificationTest.js?v=${timestamp}`),
|
||||
import(`../notifications/NotificationGestureTest.js?v=${timestamp}`),
|
||||
]);
|
||||
|
||||
const { EnhancedSecureCryptoUtils } = cryptoModule;
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
const { EnhancedSecureWebRTCManager } = webrtcModule;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
const { PayPerSessionManager } = paymentModule;
|
||||
window.PayPerSessionManager = PayPerSessionManager;
|
||||
const { EnhancedSecureFileTransfer } = fileTransferModule;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
const { NotificationIntegration } = notificationModule;
|
||||
window.NotificationIntegration = NotificationIntegration;
|
||||
const { NotificationTest } = notificationTestModule;
|
||||
window.NotificationTest = NotificationTest;
|
||||
const { NotificationGestureTest } = notificationGestureTestModule;
|
||||
window.NotificationGestureTest = NotificationGestureTest;
|
||||
|
||||
async function loadReactComponent(path) {
|
||||
const response = await fetch(`${path}?v=${timestamp}`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const code = await response.text();
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(code);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
loadReactComponent('../components/ui/SessionTimer.jsx'),
|
||||
loadReactComponent('../components/ui/Header.jsx'),
|
||||
loadReactComponent('../components/ui/SessionTypeSelector.jsx'),
|
||||
loadReactComponent('../components/ui/LightningPayment.jsx'),
|
||||
loadReactComponent('../components/ui/PaymentModal.jsx'),
|
||||
loadReactComponent('../components/ui/DownloadApps.jsx'),
|
||||
loadReactComponent('../components/ui/ComparisonTable.jsx'),
|
||||
loadReactComponent('../components/ui/UniqueFeatureSlider.jsx'),
|
||||
loadReactComponent('../components/ui/SecurityFeatures.jsx'),
|
||||
loadReactComponent('../components/ui/Testimonials.jsx'),
|
||||
loadReactComponent('../components/ui/Roadmap.jsx'),
|
||||
loadReactComponent('../components/ui/FileTransfer.jsx'),
|
||||
// Load React components using dynamic imports instead of eval
|
||||
const componentModules = await Promise.all([
|
||||
import(`../components/ui/Header.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/DownloadApps.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/ComparisonTable.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/UniqueFeatureSlider.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/SecurityFeatures.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/Testimonials.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/Roadmap.jsx?v=${timestamp}`),
|
||||
import(`../components/ui/FileTransfer.jsx?v=${timestamp}`),
|
||||
]);
|
||||
|
||||
// Components are automatically registered on window by their respective modules
|
||||
console.log('✅ All React components loaded successfully');
|
||||
|
||||
if (typeof window.initializeApp === 'function') {
|
||||
window.initializeApp();
|
||||
} else {
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
window.forceUpdateHeader = (timeLeft, sessionType) => {
|
||||
window.forceUpdateHeader = () => {
|
||||
const event = new CustomEvent('force-header-update', {
|
||||
detail: { timeLeft, sessionType, timestamp: Date.now() },
|
||||
detail: { timestamp: Date.now() },
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
if (window.sessionManager && window.sessionManager.forceUpdateTimer) {
|
||||
window.sessionManager.forceUpdateTimer();
|
||||
}
|
||||
};
|
||||
|
||||
window.updateSessionTimer = (timeLeft, sessionType) => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('session-timer-update', {
|
||||
detail: { timeLeft, sessionType },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
document.addEventListener('session-activated', (event) => {
|
||||
if (window.updateSessionTimer) {
|
||||
window.updateSessionTimer(event.detail.timeLeft, event.detail.sessionType);
|
||||
}
|
||||
if (window.forceUpdateHeader) {
|
||||
window.forceUpdateHeader(event.detail.timeLeft, event.detail.sessionType);
|
||||
window.forceUpdateHeader();
|
||||
}
|
||||
if (window.webrtcManager && window.webrtcManager.handleSessionActivation) {
|
||||
if (window.DEBUG_MODE) {
|
||||
@@ -29,9 +15,6 @@ document.addEventListener('session-activated', (event) => {
|
||||
}
|
||||
window.webrtcManager.handleSessionActivation({
|
||||
sessionId: event.detail.sessionId,
|
||||
sessionType: event.detail.sessionType,
|
||||
timeLeft: event.detail.timeLeft,
|
||||
isDemo: event.detail.isDemo,
|
||||
sessionManager: window.sessionManager,
|
||||
});
|
||||
}
|
||||
@@ -65,7 +48,7 @@ window.showUpdateNotification = function showUpdateNotification() {
|
||||
<i class="fas fa-download text-lg"></i>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Update Available</div>
|
||||
<div class="text-sm opacity-90">SecureBit.chat v4.3.120 - ECDH + DTLS + SAS is ready</div>
|
||||
<div class="text-sm opacity-90">SecureBit.chat v4.4.18 - ECDH + DTLS + SAS is ready</div>
|
||||
</div>
|
||||
<button data-action="reload" class="bg-white/20 hover:bg-white/30 px-3 py-1 rounded text-sm font-medium transition-colors">
|
||||
Update
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// PWA Offline Test Script for SecureBit.chat
|
||||
// Enhanced Security Edition v4.3.120
|
||||
// Tests offline functionality and cache status
|
||||
|
||||
class PWAOfflineTester {
|
||||
constructor() {
|
||||
this.testResults = [];
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
async runTests() {
|
||||
if (this.isRunning) {
|
||||
console.warn('⚠️ Tests already running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
this.testResults = [];
|
||||
|
||||
console.log('🧪 Starting PWA Offline Tests...');
|
||||
|
||||
try {
|
||||
await this.testServiceWorkerRegistration();
|
||||
await this.testCacheStatus();
|
||||
await this.testOfflineResources();
|
||||
await this.testOnlineResources();
|
||||
|
||||
this.showTestResults();
|
||||
} catch (error) {
|
||||
console.error('❌ Test failed:', error);
|
||||
this.addTestResult('Test Suite', false, `Test suite failed: ${error.message}`);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
async testServiceWorkerRegistration() {
|
||||
try {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (registration) {
|
||||
this.addTestResult('Service Worker Registration', true, 'Service worker is registered');
|
||||
} else {
|
||||
this.addTestResult('Service Worker Registration', false, 'No service worker found');
|
||||
}
|
||||
} else {
|
||||
this.addTestResult('Service Worker Support', false, 'Service worker not supported');
|
||||
}
|
||||
} catch (error) {
|
||||
this.addTestResult('Service Worker Registration', false, `Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async testCacheStatus() {
|
||||
try {
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
const totalCached = 0;
|
||||
|
||||
for (const cacheName of cacheNames) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
totalCached += keys.length;
|
||||
}
|
||||
|
||||
if (totalCached > 0) {
|
||||
this.addTestResult('Cache Status', true, `${totalCached} resources cached`);
|
||||
} else {
|
||||
this.addTestResult('Cache Status', false, 'No resources cached');
|
||||
}
|
||||
} else {
|
||||
this.addTestResult('Cache API Support', false, 'Cache API not supported');
|
||||
}
|
||||
} catch (error) {
|
||||
this.addTestResult('Cache Status', false, `Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async testOfflineResources() {
|
||||
const criticalResources = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'/dist/app.js',
|
||||
'/dist/app-boot.js',
|
||||
'/libs/react/react.production.min.js',
|
||||
'/libs/react-dom/react-dom.production.min.js',
|
||||
'/assets/tailwind.css'
|
||||
];
|
||||
|
||||
let cachedCount = 0;
|
||||
|
||||
for (const resource of criticalResources) {
|
||||
try {
|
||||
const cached = await caches.match(resource);
|
||||
if (cached) {
|
||||
cachedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to check cache for ${resource}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const success = cachedCount >= criticalResources.length * 0.8; // 80% success rate
|
||||
this.addTestResult('Critical Resources Cached', success,
|
||||
`${cachedCount}/${criticalResources.length} critical resources cached`);
|
||||
}
|
||||
|
||||
async testOnlineResources() {
|
||||
try {
|
||||
// Test if we can fetch a simple resource
|
||||
const response = await fetch('/favicon.ico', {
|
||||
method: 'HEAD',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.addTestResult('Network Connectivity', true, 'Network is accessible');
|
||||
} else {
|
||||
this.addTestResult('Network Connectivity', false, `Network error: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.addTestResult('Network Connectivity', false, `Network error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
addTestResult(testName, passed, message) {
|
||||
this.testResults.push({
|
||||
name: testName,
|
||||
passed,
|
||||
message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`${passed ? '✅' : '❌'} ${testName}: ${message}`);
|
||||
}
|
||||
|
||||
showTestResults() {
|
||||
const passedTests = this.testResults.filter(test => test.passed).length;
|
||||
const totalTests = this.testResults.length;
|
||||
const successRate = Math.round((passedTests / totalTests) * 100);
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 backdrop-blur-sm';
|
||||
modal.innerHTML = `
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center mr-4">
|
||||
<i class="fas fa-vial text-blue-400 text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-white">PWA Offline Test Results</h3>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 rounded-lg ${successRate >= 80 ? 'bg-green-500/10 border border-green-500/20' : 'bg-red-500/10 border border-red-500/20'}">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="font-medium text-white">Overall Score</div>
|
||||
<div class="text-sm text-gray-300">${passedTests}/${totalTests} tests passed</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold ${successRate >= 80 ? 'text-green-400' : 'text-red-400'}">
|
||||
${successRate}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
${this.testResults.map(test => `
|
||||
<div class="flex items-center justify-between p-3 rounded-lg ${test.passed ? 'bg-green-500/10 border border-green-500/20' : 'bg-red-500/10 border border-red-500/20'}">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas ${test.passed ? 'fa-check-circle text-green-400' : 'fa-times-circle text-red-400'}"></i>
|
||||
<div>
|
||||
<div class="font-medium text-white">${test.name}</div>
|
||||
<div class="text-sm text-gray-300">${test.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-3">
|
||||
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||
<h4 class="font-medium text-blue-300 mb-2">Recommendations:</h4>
|
||||
<ul class="text-sm text-blue-200 space-y-1">
|
||||
${this.getRecommendations().map(rec => `<li>• ${rec}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button onclick="window.pwaOfflineTester.runTests(); this.parentElement.parentElement.parentElement.remove();"
|
||||
class="flex-1 bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg font-medium transition-colors">
|
||||
Run Tests Again
|
||||
</button>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
class="flex-1 bg-gray-600 hover:bg-gray-500 text-white py-3 px-4 rounded-lg font-medium transition-colors">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
getRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
const failedTests = this.testResults.filter(test => !test.passed);
|
||||
|
||||
if (failedTests.some(test => test.name.includes('Service Worker'))) {
|
||||
recommendations.push('Ensure service worker is properly registered and active');
|
||||
}
|
||||
|
||||
if (failedTests.some(test => test.name.includes('Cache'))) {
|
||||
recommendations.push('Check cache configuration and ensure resources are being cached');
|
||||
}
|
||||
|
||||
if (failedTests.some(test => test.name.includes('Network'))) {
|
||||
recommendations.push('Verify network connectivity and server availability');
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
recommendations.push('All tests passed! Your PWA offline functionality is working correctly.');
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
// Public API
|
||||
getTestResults() {
|
||||
return this.testResults;
|
||||
}
|
||||
|
||||
clearResults() {
|
||||
this.testResults = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton pattern
|
||||
let instance = null;
|
||||
|
||||
const PWAOfflineTesterAPI = {
|
||||
getInstance() {
|
||||
if (!instance) {
|
||||
instance = new PWAOfflineTester();
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
runTests() {
|
||||
return this.getInstance().runTests();
|
||||
}
|
||||
};
|
||||
|
||||
// Export for module use
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PWAOfflineTesterAPI;
|
||||
} else if (typeof window !== 'undefined' && !window.PWAOfflineTester) {
|
||||
window.PWAOfflineTester = PWAOfflineTesterAPI;
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
if (typeof window !== 'undefined' && !window.pwaOfflineTester) {
|
||||
window.pwaOfflineTester = PWAOfflineTesterAPI.getInstance();
|
||||
|
||||
// Add global function for easy access
|
||||
window.testPWAOffline = () => {
|
||||
window.pwaOfflineTester.runTests();
|
||||
};
|
||||
}
|
||||
@@ -349,35 +349,6 @@ button i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Pay-per-session UI - Обновленный трехцветный таймер */
|
||||
.session-timer {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.session-timer:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Анимация пульсации для красной зоны */
|
||||
@keyframes timer-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.session-timer.animate-pulse {
|
||||
animation: timer-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Lightning button */
|
||||
.lightning-button {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
@@ -505,6 +476,21 @@ button i {
|
||||
scroll-margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message span {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.chat-messages-area {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
// SecureBit.chat Service Worker
|
||||
// Enhanced Security Edition v4.3.120 - ECDH + DTLS + SAS
|
||||
// Conservative PWA Edition v4.7.53 - Minimal Caching Strategy
|
||||
|
||||
const CACHE_NAME = 'securebit-v4.3.120';
|
||||
const STATIC_CACHE = 'securebit-static-v4.3.120';
|
||||
const DYNAMIC_CACHE = 'securebit-dynamic-v4.3.120';
|
||||
const CACHE_NAME = 'securebit-pwa-v4.7.53';
|
||||
const STATIC_CACHE = 'securebit-pwa-static-v4.7.53';
|
||||
const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.53';
|
||||
|
||||
// Files to cache for offline functionality (excluding external CDNs that may have CORS issues)
|
||||
// Essential files for PWA offline functionality
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'/src/crypto/EnhancedSecureCryptoUtils.js',
|
||||
'/src/network/EnhancedSecureWebRTCManager.js',
|
||||
'/src/session/PayPerSessionManager.js',
|
||||
'/src/components/ui/SessionTimer.jsx',
|
||||
'/src/components/ui/Header.jsx',
|
||||
'/src/components/ui/PasswordModal.jsx',
|
||||
'/src/components/ui/SessionTypeSelector.jsx',
|
||||
'/src/components/ui/PaymentModal.jsx',
|
||||
'/src/components/ui/DownloadApps.jsx',
|
||||
'/src/components/ui/ComparisonTable.jsx',
|
||||
'/src/components/ui/UniqueFeatureSlider.jsx',
|
||||
'/src/components/ui/SecurityFeatures.jsx',
|
||||
'/src/components/ui/Testimonials.jsx',
|
||||
'/src/components/ui/Roadmap.jsx',
|
||||
'/src/styles/main.css',
|
||||
'/src/styles/animations.css',
|
||||
'/src/styles/components.css',
|
||||
|
||||
// Core PWA files only
|
||||
'/dist/app.js',
|
||||
'/dist/app-boot.js',
|
||||
|
||||
// Essential styles for PWA
|
||||
'/src/styles/pwa.css',
|
||||
'/logo/favicon.ico'
|
||||
|
||||
// PWA icons (required for install)
|
||||
'/logo/icon-192x192.png',
|
||||
'/logo/icon-512x512.png',
|
||||
'/logo/favicon.ico',
|
||||
|
||||
// PWA components only
|
||||
'/src/pwa/pwa-manager.js',
|
||||
'/src/pwa/install-prompt.js',
|
||||
'/src/scripts/pwa-register.js',
|
||||
'/src/scripts/pwa-offline-test.js'
|
||||
];
|
||||
|
||||
// Sensitive files that should never be cached
|
||||
@@ -43,21 +42,22 @@ const SENSITIVE_PATTERNS = [
|
||||
|
||||
// Network first patterns (always try network first)
|
||||
const NETWORK_FIRST_PATTERNS = [
|
||||
/\.js$/,
|
||||
/\.jsx$/,
|
||||
/\/src\//,
|
||||
/api/
|
||||
/\/api\//,
|
||||
/\/session\//,
|
||||
/\/payment\//,
|
||||
/\/verification\//,
|
||||
/preimage/,
|
||||
/auth/
|
||||
];
|
||||
|
||||
// Cache first patterns (static assets)
|
||||
// Cache first patterns (only essential PWA assets)
|
||||
const CACHE_FIRST_PATTERNS = [
|
||||
/\.css$/,
|
||||
/\.png$/,
|
||||
/\.jpg$/,
|
||||
/\.svg$/,
|
||||
/\.ico$/,
|
||||
/fonts/,
|
||||
/logo/
|
||||
/manifest\.json$/,
|
||||
/logo\/icon-.*\.png$/,
|
||||
/logo\/favicon\.ico$/,
|
||||
/src\/styles\/pwa\.css$/,
|
||||
/src\/pwa\/.*\.js$/,
|
||||
/src\/scripts\/pwa-.*\.js$/
|
||||
];
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
@@ -160,23 +160,23 @@ self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(handleRequest(event.request));
|
||||
});
|
||||
|
||||
// Smart request handling with security considerations
|
||||
// Conservative request handling - only cache PWA essentials
|
||||
async function handleRequest(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
try {
|
||||
// Strategy 1: Cache First (for static assets)
|
||||
// Strategy 1: Cache First (only for essential PWA assets)
|
||||
if (CACHE_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
||||
return await cacheFirst(request);
|
||||
}
|
||||
|
||||
// Strategy 2: Network First (for dynamic content and security-critical files)
|
||||
// Strategy 2: Network First (for all other requests)
|
||||
if (NETWORK_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
||||
return await networkFirst(request);
|
||||
}
|
||||
|
||||
// Strategy 3: Stale While Revalidate (for main pages)
|
||||
return await staleWhileRevalidate(request);
|
||||
// Strategy 3: Network First for everything else (no aggressive caching)
|
||||
return await networkFirst(request);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Request handling failed:', error);
|
||||
@@ -254,29 +254,39 @@ async function staleWhileRevalidate(request) {
|
||||
return cachedResponse || networkResponsePromise || handleOffline(request);
|
||||
}
|
||||
|
||||
// Offline fallback
|
||||
// Offline fallback - minimal caching for PWA only
|
||||
async function handleOffline(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// For navigation requests, return cached index.html
|
||||
if (request.destination === 'document') {
|
||||
const cachedIndex = await caches.match('/');
|
||||
if (request.destination === 'document' || request.mode === 'navigate') {
|
||||
const cachedIndex = await caches.match('/index.html');
|
||||
if (cachedIndex) {
|
||||
return cachedIndex;
|
||||
}
|
||||
|
||||
// Fallback to root if index.html not found
|
||||
const cachedRoot = await caches.match('/');
|
||||
if (cachedRoot) {
|
||||
return cachedRoot;
|
||||
}
|
||||
}
|
||||
|
||||
// For images, return a placeholder or cached version
|
||||
if (request.destination === 'image') {
|
||||
return new Response(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><rect width="200" height="200" fill="#1a1a1a"/><text x="100" y="100" text-anchor="middle" fill="#666" font-size="14">Offline</text></svg>',
|
||||
{ headers: { 'Content-Type': 'image/svg+xml' } }
|
||||
);
|
||||
// For PWA assets, try to return cached version
|
||||
if (CACHE_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
||||
const cachedAsset = await caches.match(request);
|
||||
if (cachedAsset) {
|
||||
return cachedAsset;
|
||||
}
|
||||
}
|
||||
|
||||
// Return a generic offline response
|
||||
// Return a generic offline response for everything else
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Offline', message: 'Network unavailable' }),
|
||||
JSON.stringify({
|
||||
error: 'Offline',
|
||||
message: 'Network unavailable - PWA offline mode',
|
||||
url: url.pathname
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
@@ -293,6 +303,29 @@ self.addEventListener('sync', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function retryFailedRequests() {
|
||||
try {
|
||||
// Get all cached requests that failed
|
||||
const cache = await caches.open(DYNAMIC_CACHE);
|
||||
const requests = await cache.keys();
|
||||
|
||||
for (const request of requests) {
|
||||
try {
|
||||
// Try to fetch the request again
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
// Update cache with successful response
|
||||
await cache.put(request, response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Retry failed for:', request.url, error.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to retry requests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Notification click handler
|
||||
|
||||