diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62305a6..ccf040f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## v6.8.1 — Desktop 1.0.3
+
+The download buttons point at desktop 1.0.3. That release fixes video on a call
+placed after a group call — the two shared one connection, opened a second video
+section, and the call carried sound with no picture — and it carries the same
+STUN and TURN servers this web client uses, so a desktop and a browser can agree
+on a path instead of one of them offering no relay at all.
+
+Light theme colours: the group status dot now reads its colour from the theme in
+the call and group views as well.
+
## v6.8.0 — Light theme
Added a light theme. The switch is in the header next to the language menu, with three
diff --git a/README.md b/README.md
index 66fb95e..2b06078 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
No accounts. No servers storing your messages. No installation required.
[](LICENSE)
-[](CHANGELOG.md)
+[](CHANGELOG.md)
[](https://snapcraft.io/securebit-chat)
[](#install-as-an-app)
[](#security-model)
diff --git a/ar/index.html b/ar/index.html
index 04a39be..245dac8 100644
--- a/ar/index.html
+++ b/ar/index.html
@@ -30,18 +30,18 @@
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
';\n }\n\n /* Stringify, in case dirty is an object */\n if (typeof dirty !== 'string' && !_isNode(dirty)) {\n dirty = stringifyValue(dirty);\n\n if (typeof dirty !== 'string') {\n throw typeErrorCreate('dirty is not a string, aborting');\n }\n }\n\n /* Return dirty HTML if DOMPurify cannot run */\n if (!DOMPurify.isSupported) {\n return dirty;\n }\n\n /* Assign config vars */\n if (!SET_CONFIG) {\n _parseConfig(cfg);\n }\n\n /* Clean up removed elements */\n DOMPurify.removed = [];\n\n /* Resolve IN_PLACE for this call without mutating persistent config.\n Writing the IN_PLACE closure variable here leaks under setConfig(),\n where _parseConfig is skipped on later calls: a single string call would\n disable in-place mode for every subsequent node call, returning a\n sanitized copy while leaving the caller's node \u2014 which in-place callers\n keep using and whose return value they ignore \u2014 unsanitized. REPORT-2. */\n const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);\n\n if (inPlace) {\n /* Do some early pre-sanitization to avoid unsafe root nodes.\n Read nodeName through the cached prototype getter \u2014 a clobbering\n child named \"nodeName\" on the form root would otherwise shadow\n the property and let this check skip the root-allowlist\n validation entirely. */\n const nn = getNodeName\n ? getNodeName(dirty as Node)\n : (dirty as Node).nodeName;\n if (typeof nn === 'string') {\n const tagName = transformCaseFunc(nn);\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n throw typeErrorCreate(\n 'root node is forbidden and cannot be sanitized in-place'\n );\n }\n }\n\n /* Pre-flight the root through _isClobbered. The iterator-driven\n removal path can not detach a parent-less root: _forceRemove\n falls through to Element.prototype.remove(), which per spec\n is a no-op on a node with no parent. A clobbered root would\n then survive the main loop with its attributes uninspected,\n because _sanitizeAttributes early-returns on _isClobbered. The\n result would be an attacker-controlled form, complete with any\n event-handler attributes the caller passed in, handed back to\n the application unsanitized. Refuse to sanitize such a root\n the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */\n if (_isClobbered(dirty as Element)) {\n throw typeErrorCreate(\n 'root node is clobbered and cannot be sanitized in-place'\n );\n }\n\n /* Sanitize attached shadow roots before the main iterator runs.\n The iterator does not descend into shadow trees. Same fail-closed\n barrier as the main walk (campaign-3 F2): a custom-element reaction\n inside a shadow root could abort this pre-pass before the walk runs,\n which would otherwise leave the entire live tree unsanitized. */\n try {\n _sanitizeAttachedShadowRoots(dirty as Node);\n } catch (error) {\n _neutralizeRoot(dirty as Node);\n\n throw error;\n }\n } else if (_isNode(dirty)) {\n /* If dirty is a DOM element, append to an empty document to avoid\n elements being stripped by the parser */\n body = _initDocument('');\n importedNode = body.ownerDocument.importNode(dirty, true);\n if (\n importedNode.nodeType === NODE_TYPE.element &&\n importedNode.nodeName === 'BODY'\n ) {\n /* Node is already a body, use as is */\n body = importedNode;\n } else if (importedNode.nodeName === 'HTML') {\n body = importedNode;\n } else {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n body.appendChild(importedNode);\n }\n\n /* Clonable shadow roots are deep-cloned by importNode(); sanitize\n them before the main iterator runs, since the iterator does not\n descend into shadow trees. The walk routes every read through a\n cached prototype getter so clobbering descendants on a form root\n cannot hide a shadow host from this pass. */\n _sanitizeAttachedShadowRoots(importedNode);\n } else {\n /* Exit directly if we have nothing to do */\n if (\n !RETURN_DOM &&\n !SAFE_FOR_TEMPLATES &&\n !WHOLE_DOCUMENT &&\n // eslint-disable-next-line unicorn/prefer-includes\n dirty.indexOf('<') === -1\n ) {\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? _createTrustedHTML(dirty)\n : dirty;\n }\n\n /* Initialize the document to work on */\n body = _initDocument(dirty);\n\n /* Check we have a DOM node from the data */\n if (!body) {\n return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n }\n }\n\n /* Remove first element node (ours) if FORCE_BODY is set */\n if (body && FORCE_BODY) {\n _forceRemove(body.firstChild);\n }\n\n /* Get node iterator */\n const nodeIterator = _createNodeIterator(inPlace ? dirty : body);\n\n /* Now start iterating over the created document.\n The walk runs inside an exception barrier (campaign-3 F2): a re-entrant\n engine/custom-element mutation can detach a node mid-walk so\n `_forceRemove`'s parentless guard throws, aborting the loop. Without the\n barrier the caller's in-place tree would be left half-sanitized with the\n unvisited tail still armed. On any throw we fail closed \u2014 strip the\n in-place root bare \u2014 then rethrow so the existing throw contract is\n preserved. (String/DOM-copy paths never return the partial body, so the\n propagating throw is already fail-closed there.) */\n try {\n while ((currentNode = nodeIterator.nextNode())) {\n /* Sanitize tags and elements */\n _sanitizeElements(currentNode);\n\n /* Check attributes next */\n _sanitizeAttributes(currentNode);\n\n /* Shadow DOM detected, sanitize it.\n Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection\n instead of instanceof, so foreign-realm .content is\n walked correctly. */\n if (_isDocumentFragment(currentNode.content)) {\n _sanitizeShadowDOM(currentNode.content);\n }\n }\n } catch (error) {\n if (inPlace) {\n _neutralizeRoot(dirty as Node);\n }\n\n throw error;\n }\n\n /* If we sanitized `dirty` in-place, return it. */\n if (inPlace) {\n /* Fail-closed completion of the audit-5 F1 fix: every node removed from\n the caller's live tree is detached but may still hold a queued\n resource-event handler that fires in page scope after we return. The\n move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the\n non-allow-listed attributes off every other removed subtree (clobber,\n mXSS, namespace, comments, KEEP_CONTENT:false, \u2026) so those handlers are\n cancelled before any event can fire. Runs synchronously, pre-return. */\n arrayForEach(DOMPurify.removed, (entry) => {\n if (entry.element) {\n _neutralizeSubtree(entry.element as Node);\n }\n });\n\n if (SAFE_FOR_TEMPLATES) {\n _scrubTemplateExpressions(dirty as Element);\n }\n\n return dirty;\n }\n\n /* Return sanitized string or DOM */\n if (RETURN_DOM) {\n if (SAFE_FOR_TEMPLATES) {\n _scrubTemplateExpressions(body);\n }\n\n if (RETURN_DOM_FRAGMENT) {\n returnNode = createDocumentFragment.call(body.ownerDocument);\n\n while (body.firstChild) {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n returnNode.appendChild(body.firstChild);\n }\n } else {\n returnNode = body;\n }\n\n if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n /*\n AdoptNode() is not used because internal state is not reset\n (e.g. the past names map of a HTMLFormElement), this is safe\n in theory but we would rather not risk another attack vector.\n The state that is cloned by importNode() is explicitly defined\n by the specs.\n */\n returnNode = importNode.call(originalDocument, returnNode, true);\n }\n\n return returnNode;\n }\n\n let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n /* Serialize doctype if allowed */\n if (\n WHOLE_DOCUMENT &&\n ALLOWED_TAGS['!doctype'] &&\n body.ownerDocument &&\n body.ownerDocument.doctype &&\n body.ownerDocument.doctype.name &&\n regExpTest(EXPRESSIONS.DOCTYPE_NAME, body.ownerDocument.doctype.name)\n ) {\n serializedHTML =\n '\\n' + serializedHTML;\n }\n\n /* Sanitize final string template-safe */\n if (SAFE_FOR_TEMPLATES) {\n serializedHTML = _stripTemplateExpressions(serializedHTML);\n }\n\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? _createTrustedHTML(serializedHTML)\n : serializedHTML;\n };\n\n DOMPurify.setConfig = function (cfg = {}) {\n _parseConfig(cfg);\n SET_CONFIG = true;\n };\n\n DOMPurify.clearConfig = function () {\n CONFIG = null;\n SET_CONFIG = false;\n\n // Drop any caller-supplied Trusted Types policy so it cannot poison later\n // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and\n // never recreated \u2014 Trusted Types throws on duplicate names) is restored by\n // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.\n trustedTypesPolicy = defaultTrustedTypesPolicy;\n emptyHTML = '';\n };\n\n DOMPurify.isValidAttribute = function (tag, attr, value) {\n /* Initialize shared config vars if necessary. */\n if (!CONFIG) {\n _parseConfig({});\n }\n\n const lcTag = transformCaseFunc(tag);\n const lcName = transformCaseFunc(attr);\n return _isValidAttribute(lcTag, lcName, value);\n };\n\n DOMPurify.addHook = function (\n entryPoint: keyof HooksMap,\n hookFunction: HookFunction\n ) {\n if (typeof hookFunction !== 'function') {\n return;\n }\n\n arrayPush(hooks[entryPoint], hookFunction);\n };\n\n DOMPurify.removeHook = function (\n entryPoint: keyof HooksMap,\n hookFunction: HookFunction\n ) {\n if (hookFunction !== undefined) {\n const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);\n\n return index === -1\n ? undefined\n : arraySplice(hooks[entryPoint], index, 1)[0];\n }\n\n return arrayPop(hooks[entryPoint]);\n };\n\n DOMPurify.removeHooks = function (entryPoint: keyof HooksMap) {\n hooks[entryPoint] = [];\n };\n\n DOMPurify.removeAllHooks = function () {\n hooks = _createHooksMap();\n };\n\n return DOMPurify;\n}\n\nexport default createDOMPurify();\n", "import createDOMPurify from 'dompurify';\n\nclass EnhancedSecureCryptoUtils {\n\n static _keyMetadata = new WeakMap();\n static _messageSanitizer = null;\n \n // Initialize secure logging system after class definition\n\n // Utility to sort object keys for deterministic serialization\n static sortObjectKeys(obj) {\n if (typeof obj !== 'object' || obj === null) {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map(EnhancedSecureCryptoUtils.sortObjectKeys);\n }\n\n const sortedObj = {};\n Object.keys(obj).sort().forEach(key => {\n sortedObj[key] = EnhancedSecureCryptoUtils.sortObjectKeys(obj[key]);\n });\n return sortedObj;\n }\n\n // Utility to assert CryptoKey type and properties\n static assertCryptoKey(key, expectedName = null, expectedUsages = []) {\n if (!(key instanceof CryptoKey)) throw new Error('Expected CryptoKey');\n if (expectedName && key.algorithm?.name !== expectedName) {\n throw new Error(`Expected algorithm ${expectedName}, got ${key.algorithm?.name}`);\n }\n for (const u of expectedUsages) {\n if (!key.usages || !key.usages.includes(u)) {\n throw new Error(`Missing required key usage: ${u}`);\n }\n }\n }\n // Helper function to convert ArrayBuffer to Base64\n static arrayBufferToBase64(buffer) {\n let binary = '';\n const bytes = new Uint8Array(buffer);\n const len = bytes.byteLength;\n for (let i = 0; i < len; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n }\n\n // Helper function to convert Base64 to ArrayBuffer\n static base64ToArrayBuffer(base64) {\n try {\n // Validate input\n if (typeof base64 !== 'string' || !base64) {\n throw new Error('Invalid base64 input: must be a non-empty string');\n }\n\n // Remove any whitespace and validate base64 format\n const cleanBase64 = base64.trim();\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {\n throw new Error('Invalid base64 format');\n }\n\n // Handle empty string case\n if (cleanBase64 === '') {\n return new ArrayBuffer(0);\n }\n\n const binaryString = atob(cleanBase64);\n const len = binaryString.length;\n const bytes = new Uint8Array(len);\n for (let i = 0; i < len; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n } catch (error) {\n console.error('Base64 to ArrayBuffer conversion failed:', error.message);\n throw new Error(`Base64 conversion error: ${error.message}`);\n }\n }\n\n // Helper function to convert hex string to Uint8Array\n static hexToUint8Array(hexString) {\n try {\n if (!hexString || typeof hexString !== 'string') {\n throw new Error('Invalid hex string input: must be a non-empty string');\n }\n\n // Remove colons and spaces from hex string (e.g., \"aa:bb:cc\" -> \"aabbcc\")\n const cleanHex = hexString.replace(/:/g, '').replace(/\\s/g, '');\n \n // Validate hex format\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\n throw new Error('Invalid hex format: contains non-hex characters');\n }\n \n // Ensure even length\n if (cleanHex.length % 2 !== 0) {\n throw new Error('Invalid hex format: odd length');\n }\n\n // Convert hex string to bytes\n const bytes = new Uint8Array(cleanHex.length / 2);\n for (let i = 0; i < cleanHex.length; i += 2) {\n bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);\n }\n \n return bytes;\n } catch (error) {\n console.error('Hex to Uint8Array conversion failed:', error.message);\n throw new Error(`Hex conversion error: ${error.message}`);\n }\n }\n\n /**\n * Overwrite a buffer holding key material once it is no longer needed.\n *\n * This is a genuine wipe, unlike the manager's _secureWipeString /\n * _secureWipeCryptoKey, which cannot wipe anything (JS strings are immutable\n * and a non-extractable CryptoKey has no JS-visible bytes) and only ever\n * dropped a reference while reporting success. Here the bytes really are\n * ours: overwrite them so the shared secret does not linger in the heap\n * waiting for a garbage collector that may never run before a heap snapshot\n * or a memory-reading extension gets there first.\n *\n * Random first, then zeros: on the off chance a copying GC has already moved\n * the buffer, the random pass at least destroys the plaintext value at the\n * old address as well as the new one.\n */\n static zeroizeBuffer(buffer) {\n try {\n if (!buffer) return;\n const view = buffer instanceof Uint8Array\n ? buffer\n : (buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : null);\n if (!view || view.length === 0) return;\n crypto.getRandomValues(view);\n view.fill(0);\n } catch (_) {\n // A detached buffer is already unreadable; nothing left to do.\n }\n }\n\n static async encryptData(data, password) {\n try {\n const dataString = typeof data === 'string' ? data : JSON.stringify(data);\n const salt = crypto.getRandomValues(new Uint8Array(16));\n const encoder = new TextEncoder();\n const passwordBuffer = encoder.encode(password);\n\n const keyMaterial = await crypto.subtle.importKey(\n 'raw',\n passwordBuffer,\n { name: 'PBKDF2' },\n false,\n ['deriveKey']\n );\n\n const key = await crypto.subtle.deriveKey(\n {\n name: 'PBKDF2',\n salt: salt,\n iterations: 310000,\n hash: 'SHA-256',\n },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt']\n );\n\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const dataBuffer = encoder.encode(dataString);\n const encrypted = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: iv },\n key,\n dataBuffer\n );\n\n const encryptedPackage = {\n version: '1.0',\n salt: Array.from(salt),\n iv: Array.from(iv),\n data: Array.from(new Uint8Array(encrypted)),\n timestamp: Date.now(),\n };\n\n const packageString = JSON.stringify(encryptedPackage);\n return EnhancedSecureCryptoUtils.arrayBufferToBase64(new TextEncoder().encode(packageString).buffer);\n\n } catch (error) {\n console.error('Encryption failed:', error.message);\n throw new Error(`Encryption error: ${error.message}`);\n }\n }\n\n static async decryptData(encryptedData, password) {\n try {\n const packageBuffer = EnhancedSecureCryptoUtils.base64ToArrayBuffer(encryptedData);\n const packageString = new TextDecoder().decode(packageBuffer);\n const encryptedPackage = JSON.parse(packageString);\n\n if (!encryptedPackage.version || !encryptedPackage.salt || !encryptedPackage.iv || !encryptedPackage.data) {\n throw new Error('Invalid encrypted data format');\n }\n\n const salt = new Uint8Array(encryptedPackage.salt);\n const iv = new Uint8Array(encryptedPackage.iv);\n const encrypted = new Uint8Array(encryptedPackage.data);\n\n const encoder = new TextEncoder();\n const passwordBuffer = encoder.encode(password);\n\n const keyMaterial = await crypto.subtle.importKey(\n 'raw',\n passwordBuffer,\n { name: 'PBKDF2' },\n false,\n ['deriveKey']\n );\n\n const key = await crypto.subtle.deriveKey(\n {\n name: 'PBKDF2',\n salt: salt,\n iterations: 310000,\n hash: 'SHA-256'\n },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['decrypt']\n );\n\n const decrypted = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n key,\n encrypted\n );\n\n const decryptedString = new TextDecoder().decode(decrypted);\n\n try {\n return JSON.parse(decryptedString);\n } catch {\n return decryptedString;\n }\n\n } catch (error) {\n console.error('Decryption failed:', error.message);\n throw new Error(`Decryption error: ${error.message}`);\n }\n }\n\n \n // Generate secure password for data exchange\n static generateSecurePassword() {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';\n const charCount = chars.length;\n const length = 32; \n let password = '';\n \n // Use rejection sampling to avoid bias\n for (let i = 0; i < length; i++) {\n let randomValue;\n do {\n randomValue = crypto.getRandomValues(new Uint32Array(1))[0];\n } while (randomValue >= 4294967296 - (4294967296 % charCount)); // Reject biased values\n \n password += chars[randomValue % charCount];\n }\n return password;\n }\n\n // Real security level calculation with actual verification\n static async calculateSecurityLevel(securityManager) {\n let score = 0;\n const maxScore = 100; // Fixed: Changed from 110 to 100 for cleaner percentage\n const verificationResults = {};\n \n try {\n // Fallback to basic calculation if securityManager is not fully initialized\n if (!securityManager || !securityManager.securityFeatures) {\n console.warn('Security manager not fully initialized, using fallback calculation');\n return {\n level: 'INITIALIZING',\n score: 0,\n color: 'gray',\n verificationResults: {},\n timestamp: Date.now(),\n details: 'Security system initializing...',\n isRealData: false\n };\n }\n\n // All security features are enabled by default - no session type restrictions\n const sessionType = 'full'; // All features enabled\n const isDemoSession = false; // All features available\n \n // 1. Base encryption verification (20 points) - Available in demo\n try {\n const encryptionResult = await EnhancedSecureCryptoUtils.verifyEncryption(securityManager);\n if (encryptionResult.passed) {\n score += 20;\n verificationResults.verifyEncryption = { passed: true, details: encryptionResult.details, points: 20 };\n } else {\n verificationResults.verifyEncryption = { passed: false, details: encryptionResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyEncryption = { passed: false, details: `Encryption check failed: ${error.message}`, points: 0 };\n }\n \n // 2. Simple key exchange verification (15 points) - Available in demo\n try {\n const ecdhResult = await EnhancedSecureCryptoUtils.verifyECDHKeyExchange(securityManager);\n if (ecdhResult.passed) {\n score += 15;\n verificationResults.verifyECDHKeyExchange = { passed: true, details: ecdhResult.details, points: 15 };\n } else {\n verificationResults.verifyECDHKeyExchange = { passed: false, details: ecdhResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyECDHKeyExchange = { passed: false, details: `Key exchange check failed: ${error.message}`, points: 0 };\n }\n \n // 3. Message integrity verification (10 points) - Available in demo\n try {\n const integrityResult = await EnhancedSecureCryptoUtils.verifyMessageIntegrity(securityManager);\n if (integrityResult.passed) {\n score += 10;\n verificationResults.verifyMessageIntegrity = { passed: true, details: integrityResult.details, points: 10 };\n } else {\n verificationResults.verifyMessageIntegrity = { passed: false, details: integrityResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyMessageIntegrity = { passed: false, details: `Message integrity check failed: ${error.message}`, points: 0 };\n }\n \n // 4. ECDSA signatures verification (15 points) - All features enabled by default\n try {\n const ecdsaResult = await EnhancedSecureCryptoUtils.verifyECDSASignatures(securityManager);\n if (ecdsaResult.passed) {\n score += 15;\n verificationResults.verifyECDSASignatures = { passed: true, details: ecdsaResult.details, points: 15 };\n } else {\n verificationResults.verifyECDSASignatures = { passed: false, details: ecdsaResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyECDSASignatures = { passed: false, details: `Digital signatures check failed: ${error.message}`, points: 0 };\n }\n \n // 5. Rate limiting verification (5 points) - Available in demo\n try {\n const rateLimitResult = await EnhancedSecureCryptoUtils.verifyRateLimiting(securityManager);\n if (rateLimitResult.passed) {\n score += 5;\n verificationResults.verifyRateLimiting = { passed: true, details: rateLimitResult.details, points: 5 };\n } else {\n verificationResults.verifyRateLimiting = { passed: false, details: rateLimitResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyRateLimiting = { passed: false, details: `Rate limiting check failed: ${error.message}`, points: 0 };\n }\n \n // 6. Metadata protection verification (10 points) - All features enabled by default\n try {\n const metadataResult = await EnhancedSecureCryptoUtils.verifyMetadataProtection(securityManager);\n if (metadataResult.passed) {\n score += 10;\n verificationResults.verifyMetadataProtection = { passed: true, details: metadataResult.details, points: 10 };\n } else {\n verificationResults.verifyMetadataProtection = { passed: false, details: metadataResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyMetadataProtection = { passed: false, details: `Metadata protection check failed: ${error.message}`, points: 0 };\n }\n \n // 7. Perfect Forward Secrecy verification (10 points) - All features enabled by default\n try {\n const pfsResult = await EnhancedSecureCryptoUtils.verifyPerfectForwardSecrecy(securityManager);\n if (pfsResult.passed) {\n score += 10;\n verificationResults.verifyPerfectForwardSecrecy = { passed: true, details: pfsResult.details, points: 10 };\n } else {\n verificationResults.verifyPerfectForwardSecrecy = { passed: false, details: pfsResult.details, points: 0 };\n }\n } catch (error) {\n verificationResults.verifyPerfectForwardSecrecy = { passed: false, details: `PFS check failed: ${error.message}`, points: 0 };\n }\n \n // 8. Nested encryption verification (5 points) - All features enabled by default\n if (await EnhancedSecureCryptoUtils.verifyNestedEncryption(securityManager)) {\n score += 5;\n verificationResults.nestedEncryption = { passed: true, details: 'Nested encryption active', points: 5 };\n } else {\n verificationResults.nestedEncryption = { passed: false, details: 'Nested encryption failed', points: 0 };\n }\n \n // 9. Packet padding verification (5 points) - All features enabled by default\n if (await EnhancedSecureCryptoUtils.verifyPacketPadding(securityManager)) {\n score += 5;\n verificationResults.packetPadding = { passed: true, details: 'Packet padding active', points: 5 };\n } else {\n verificationResults.packetPadding = { passed: false, details: 'Packet padding failed', points: 0 };\n }\n \n // 10. Advanced features verification (10 points) - All features enabled by default\n if (await EnhancedSecureCryptoUtils.verifyAdvancedFeatures(securityManager)) {\n score += 10;\n verificationResults.advancedFeatures = { passed: true, details: 'Advanced features active', points: 10 };\n } else {\n verificationResults.advancedFeatures = { passed: false, details: 'Advanced features failed', points: 0 };\n }\n \n const percentage = Math.round((score / maxScore) * 100);\n \n // All security features are available - no restrictions\n const availableChecks = 10; // All 10 security checks available\n const passedChecks = Object.values(verificationResults).filter(r => r.passed).length;\n \n const result = {\n level: percentage >= 85 ? 'HIGH' : percentage >= 65 ? 'MEDIUM' : percentage >= 35 ? 'LOW' : 'CRITICAL',\n score: percentage,\n color: percentage >= 85 ? 'green' : percentage >= 65 ? 'orange' : percentage >= 35 ? 'yellow' : 'red',\n verificationResults,\n timestamp: Date.now(),\n details: `Real verification: ${score}/${maxScore} security checks passed (${passedChecks}/${availableChecks} available)`,\n isRealData: true,\n passedChecks: passedChecks,\n totalChecks: availableChecks,\n sessionType: sessionType,\n maxPossibleScore: 100 // All features enabled - max 100 points\n };\n\n \n return result;\n } catch (error) {\n console.error('Security level calculation failed:', error.message);\n return {\n level: 'UNKNOWN',\n score: 0,\n color: 'red',\n verificationResults: {},\n timestamp: Date.now(),\n details: `Verification failed: ${error.message}`,\n isRealData: false\n };\n }\n }\n\n // Real verification functions\n static async verifyEncryption(securityManager) {\n try {\n if (!securityManager.encryptionKey) {\n return { passed: false, details: 'No encryption key available' };\n }\n \n // Test actual encryption/decryption with multiple data types\n const testCases = [\n 'Test encryption verification',\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438',\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\n 'Large data: ' + 'A'.repeat(1000)\n ];\n \n for (const testData of testCases) {\n const encoder = new TextEncoder();\n const testBuffer = encoder.encode(testData);\n const iv = crypto.getRandomValues(new Uint8Array(12));\n \n const encrypted = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n securityManager.encryptionKey,\n testBuffer\n );\n \n const decrypted = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n securityManager.encryptionKey,\n encrypted\n );\n \n const decryptedText = new TextDecoder().decode(decrypted);\n if (decryptedText !== testData) {\n return { passed: false, details: `Decryption mismatch for: ${testData.substring(0, 20)}...` };\n }\n }\n \n return { passed: true, details: 'AES-GCM encryption/decryption working correctly' };\n } catch (error) {\n console.error('Encryption verification failed:', error.message);\n return { passed: false, details: `Encryption test failed: ${error.message}` };\n }\n }\n \n static async verifyECDHKeyExchange(securityManager) {\n try {\n if (!securityManager.ecdhKeyPair || !securityManager.ecdhKeyPair.privateKey || !securityManager.ecdhKeyPair.publicKey) {\n return { passed: false, details: 'No ECDH key pair available' };\n }\n \n // Test that keys are actually ECDH keys\n const keyType = securityManager.ecdhKeyPair.privateKey.algorithm.name;\n const curve = securityManager.ecdhKeyPair.privateKey.algorithm.namedCurve;\n \n if (keyType !== 'ECDH') {\n return { passed: false, details: `Invalid key type: ${keyType}, expected ECDH` };\n }\n \n if (curve !== 'P-384' && curve !== 'P-256') {\n return { passed: false, details: `Unsupported curve: ${curve}, expected P-384 or P-256` };\n }\n \n // Test key derivation\n try {\n const derivedKey = await crypto.subtle.deriveKey(\n { name: 'ECDH', public: securityManager.ecdhKeyPair.publicKey },\n securityManager.ecdhKeyPair.privateKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt']\n );\n \n if (!derivedKey) {\n return { passed: false, details: 'Key derivation failed' };\n }\n } catch (deriveError) {\n return { passed: false, details: `Key derivation test failed: ${deriveError.message}` };\n }\n \n return { passed: true, details: `ECDH key exchange working with ${curve} curve` };\n } catch (error) {\n console.error('ECDH verification failed:', error.message);\n return { passed: false, details: `ECDH test failed: ${error.message}` };\n }\n }\n \n static async verifyECDSASignatures(securityManager) {\n try {\n if (!securityManager.ecdsaKeyPair || !securityManager.ecdsaKeyPair.privateKey || !securityManager.ecdsaKeyPair.publicKey) {\n return { passed: false, details: 'No ECDSA key pair available' };\n }\n \n // Test actual signing and verification with multiple test cases\n const testCases = [\n 'Test ECDSA signature verification',\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u043E\u0434\u043F\u0438\u0441\u0438',\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\n 'Large data: ' + 'B'.repeat(2000)\n ];\n \n for (const testData of testCases) {\n const encoder = new TextEncoder();\n const testBuffer = encoder.encode(testData);\n \n const signature = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n securityManager.ecdsaKeyPair.privateKey,\n testBuffer\n );\n \n const isValid = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n securityManager.ecdsaKeyPair.publicKey,\n signature,\n testBuffer\n );\n \n if (!isValid) {\n return { passed: false, details: `Signature verification failed for: ${testData.substring(0, 20)}...` };\n }\n }\n \n return { passed: true, details: 'ECDSA digital signatures working correctly' };\n } catch (error) {\n console.error('ECDSA verification failed:', error.message);\n return { passed: false, details: `ECDSA test failed: ${error.message}` };\n }\n }\n \n static async verifyMessageIntegrity(securityManager) {\n try {\n // Check if macKey exists and is a valid CryptoKey\n if (!securityManager.macKey || !(securityManager.macKey instanceof CryptoKey)) {\n return { passed: false, details: 'MAC key not available or invalid' };\n }\n \n // Test message integrity with HMAC using multiple test cases\n const testCases = [\n 'Test message integrity verification',\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0446\u0435\u043B\u043E\u0441\u0442\u043D\u043E\u0441\u0442\u0438',\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\n 'Large data: ' + 'C'.repeat(3000)\n ];\n \n for (const testData of testCases) {\n const encoder = new TextEncoder();\n const testBuffer = encoder.encode(testData);\n \n const hmac = await crypto.subtle.sign(\n { name: 'HMAC', hash: 'SHA-256' },\n securityManager.macKey,\n testBuffer\n );\n \n const isValid = await crypto.subtle.verify(\n { name: 'HMAC', hash: 'SHA-256' },\n securityManager.macKey,\n hmac,\n testBuffer\n );\n \n if (!isValid) {\n return { passed: false, details: `HMAC verification failed for: ${testData.substring(0, 20)}...` };\n }\n }\n \n return { passed: true, details: 'Message integrity (HMAC) working correctly' };\n } catch (error) {\n console.error('Message integrity verification failed:', error.message);\n return { passed: false, details: `Message integrity test failed: ${error.message}` };\n }\n }\n \n // Additional verification functions.\n //\n // These used to be three `return { passed: true }` stubs \u2014 a quarter of the\n // reported score awarded for checks that never ran, under a UI that calls the\n // result \"Real cryptographic tests\". A security indicator that cannot fail\n // tells the user nothing; worse, it keeps reading green after the subsystem\n // it claims to measure breaks. Each one below now exercises the thing it\n // names and is expected to be able to fail.\n\n static async verifyRateLimiting(securityManager) {\n try {\n const limiter = EnhancedSecureCryptoUtils.rateLimiter;\n if (!limiter || typeof limiter.checkMessageRate !== 'function') {\n return { passed: false, details: 'Rate limiter is not available' };\n }\n\n // Drive a throwaway bucket past its limit and confirm it actually\n // refuses. A separate identifier per run keeps the live counters\n // untouched, so running the report never costs the user quota.\n const probeId = `selftest_${crypto.getRandomValues(new Uint32Array(1))[0]}`;\n const limit = 3;\n for (let i = 0; i < limit; i++) {\n const allowed = await limiter.checkMessageRate(probeId, limit, 60000);\n if (!allowed) {\n return { passed: false, details: `Rate limiter refused message ${i + 1} of ${limit} while under the limit` };\n }\n }\n\n const shouldBeBlocked = await limiter.checkMessageRate(probeId, limit, 60000);\n limiter.messages.delete(`msg_${probeId}`);\n\n if (shouldBeBlocked) {\n return { passed: false, details: 'Rate limiter did not block a message over the limit' };\n }\n\n return { passed: true, details: `Rate limiting verified: ${limit} allowed, the next refused` };\n } catch (error) {\n return { passed: false, details: `Rate limiting test failed: ${error.message}` };\n }\n }\n\n static async verifyMetadataProtection(securityManager) {\n try {\n const metadataKey = securityManager?.metadataKey;\n if (!metadataKey || !(metadataKey instanceof CryptoKey)) {\n return { passed: false, details: 'Metadata encryption key not available' };\n }\n if (metadataKey.algorithm?.name !== 'AES-GCM') {\n return { passed: false, details: `Metadata key has the wrong algorithm: ${metadataKey.algorithm?.name}` };\n }\n if (metadataKey.extractable) {\n return { passed: false, details: 'Metadata key is extractable' };\n }\n\n // Key separation is the whole point: message metadata (ids, sequence\n // numbers, real lengths) must not be readable with the message key.\n if (securityManager.encryptionKey === metadataKey) {\n return { passed: false, details: 'Metadata key is not separated from the message key' };\n }\n\n // Round-trip a probe so a key that exists but cannot be used is caught.\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const probe = new TextEncoder().encode('metadata-protection-selftest');\n const sealed = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, metadataKey, probe);\n const opened = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, metadataKey, sealed);\n if (new TextDecoder().decode(opened) !== 'metadata-protection-selftest') {\n return { passed: false, details: 'Metadata encryption round-trip mismatch' };\n }\n\n return { passed: true, details: 'Metadata is encrypted under a separate non-extractable key' };\n } catch (error) {\n return { passed: false, details: `Metadata protection test failed: ${error.message}` };\n }\n }\n\n static async verifyPerfectForwardSecrecy(securityManager) {\n try {\n // Session-level PFS is real: every session runs a fresh ephemeral ECDH\n // and the derived keys are non-extractable and wiped when it ends.\n const hasEphemeralKeys = !!securityManager?.ecdhKeyPair?.privateKey &&\n securityManager.ecdhKeyPair.privateKey.extractable === false;\n if (!hasEphemeralKeys) {\n return { passed: false, details: 'No non-extractable ephemeral ECDH key pair for this session' };\n }\n\n // In-session forward secrecy comes from the Double Ratchet: a per-\n // message key derived by a one-way KDF and destroyed after use, plus a\n // DH step whenever the conversation changes direction. Without it a\n // single compromised session key opens the entire transcript, which is\n // the state this check used to report as \"configured and active\".\n if (securityManager?.isRatchetActive?.()) {\n const state = securityManager._ratchet?.getState?.() || {};\n return {\n passed: true,\n details: `Double Ratchet active: per-message keys destroyed after use, DH re-key on each reply (sent ${state.sendCount ?? 0}, received ${state.receiveCount ?? 0} on the current chain)`\n };\n }\n\n return {\n passed: false,\n details: 'Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation'\n };\n } catch (error) {\n return { passed: false, details: `PFS test failed: ${error.message}` };\n }\n }\n\n static async verifyReplayProtection(securityManager) {\n try {\n // Debug logs removed to prevent leaking runtime state\n \n // Check if replay protection is enabled\n if (!securityManager.replayProtection) {\n return { passed: false, details: 'Replay protection not enabled' };\n }\n \n return { passed: true, details: 'Replay protection is working correctly' };\n } catch (error) {\n return { passed: false, details: `Replay protection test failed: ${error.message}` };\n }\n }\n \n static async verifyDTLSFingerprint(securityManager) {\n try {\n // Debug logs removed\n \n // Check if DTLS fingerprint is available\n if (!securityManager.dtlsFingerprint) {\n return { passed: false, details: 'DTLS fingerprint not available' };\n }\n \n return { passed: true, details: 'DTLS fingerprint is valid and available' };\n } catch (error) {\n return { passed: false, details: `DTLS fingerprint test failed: ${error.message}` };\n }\n }\n \n static async verifySASVerification(securityManager) {\n try {\n // Debug logs removed\n \n // Check if SAS code is available\n if (!securityManager.sasCode) {\n return { passed: false, details: 'SAS code not available' };\n }\n \n return { passed: true, details: 'SAS verification code is valid and available' };\n } catch (error) {\n return { passed: false, details: `SAS verification test failed: ${error.message}` };\n }\n }\n \n static async verifyTrafficObfuscation(securityManager) {\n try {\n // Debug logs removed\n \n // Check if traffic obfuscation is enabled\n if (!securityManager.trafficObfuscation) {\n return { passed: false, details: 'Traffic obfuscation not enabled' };\n }\n \n return { passed: true, details: 'Traffic obfuscation is working correctly' };\n } catch (error) {\n return { passed: false, details: `Traffic obfuscation test failed: ${error.message}` };\n }\n }\n \n static async verifyNestedEncryption(securityManager) {\n try {\n // Check if nestedEncryptionKey exists and is a valid CryptoKey\n if (!securityManager.nestedEncryptionKey || !(securityManager.nestedEncryptionKey instanceof CryptoKey)) {\n console.warn('Nested encryption key not available or invalid');\n return false;\n }\n \n // Test nested encryption\n const testData = 'Test nested encryption verification';\n const encoder = new TextEncoder();\n const testBuffer = encoder.encode(testData);\n \n // Simulate nested encryption\n const encrypted = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },\n securityManager.nestedEncryptionKey,\n testBuffer\n );\n \n return encrypted && encrypted.byteLength > 0;\n } catch (error) {\n console.error('Nested encryption verification failed:', error.message);\n return false;\n }\n }\n \n static async verifyPacketPadding(securityManager) {\n try {\n if (!securityManager.paddingConfig || !securityManager.paddingConfig.enabled) return false;\n \n // Test packet padding functionality\n const testData = 'Test packet padding verification';\n const encoder = new TextEncoder();\n const testBuffer = encoder.encode(testData);\n \n // Simulate packet padding\n const paddingSize = Math.floor(Math.random() * (securityManager.paddingConfig.maxPadding - securityManager.paddingConfig.minPadding)) + securityManager.paddingConfig.minPadding;\n const paddedData = new Uint8Array(testBuffer.byteLength + paddingSize);\n paddedData.set(new Uint8Array(testBuffer), 0);\n \n return paddedData.byteLength >= testBuffer.byteLength + securityManager.paddingConfig.minPadding;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Packet padding verification failed', { error: error.message });\n return false;\n }\n }\n \n static async verifyAdvancedFeatures(securityManager) {\n try {\n // Test advanced features like traffic obfuscation, fake traffic, etc.\n const hasFakeTraffic = securityManager.fakeTrafficConfig && securityManager.fakeTrafficConfig.enabled;\n const hasDecoyChannels = securityManager.decoyChannelsConfig && securityManager.decoyChannelsConfig.enabled;\n const hasAntiFingerprinting = securityManager.antiFingerprintingConfig && securityManager.antiFingerprintingConfig.enabled;\n \n return hasFakeTraffic || hasDecoyChannels || hasAntiFingerprinting;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Advanced features verification failed', { error: error.message });\n return false;\n }\n }\n \n static async verifyMutualAuth(securityManager) {\n try {\n if (!securityManager.isVerified || !securityManager.verificationCode) return false;\n \n // Test mutual authentication\n return securityManager.isVerified && securityManager.verificationCode.length > 0;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Mutual auth verification failed', { error: error.message });\n return false;\n }\n }\n \n \n static async verifyNonExtractableKeys(securityManager) {\n // This check was inverted: it returned true when exportKey SUCCEEDED \u2014\n // i.e. when the key was extractable, the failure case \u2014 and also true in\n // the catch. It could not return false, so it confirmed nothing.\n const keys = [\n ['encryptionKey', securityManager?.encryptionKey],\n ['macKey', securityManager?.macKey],\n ['metadataKey', securityManager?.metadataKey]\n ];\n\n for (const [name, key] of keys) {\n if (!key || !(key instanceof CryptoKey)) {\n return false;\n }\n // `extractable` is the authoritative answer and needs no export\n // attempt; exporting a key just to prove it cannot be exported would\n // copy it into the JS heap on every implementation that allows it.\n if (key.extractable !== false) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Session key is extractable', { keyName: name });\n return false;\n }\n }\n\n return true;\n }\n \n static async verifyEnhancedValidation(securityManager) {\n try {\n if (!securityManager.securityFeatures) return false;\n \n // Test enhanced validation features\n const hasValidation = securityManager.securityFeatures.hasEnhancedValidation || \n securityManager.securityFeatures.hasEnhancedReplayProtection;\n \n return hasValidation;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced validation verification failed', { error: error.message });\n return false;\n }\n }\n \n \n static async verifyPFS(securityManager) {\n try {\n // Check if PFS is active\n return securityManager.securityFeatures &&\n securityManager.securityFeatures.hasPFS === true &&\n securityManager.keyRotationInterval &&\n securityManager.currentKeyVersion !== undefined &&\n securityManager.keyVersions &&\n securityManager.keyVersions instanceof Map;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'PFS verification failed', { error: error.message });\n return false;\n }\n }\n\n // Rate limiting implementation\n static rateLimiter = {\n messages: new Map(),\n connections: new Map(),\n locks: new Map(),\n \n async checkMessageRate(identifier, limit = 60, windowMs = 60000) {\n if (typeof identifier !== 'string' || identifier.length > 256) {\n return false;\n }\n \n const key = `msg_${identifier}`;\n\n if (this.locks.has(key)) {\n\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 10) + 5));\n return this.checkMessageRate(identifier, limit, windowMs);\n }\n \n this.locks.set(key, true);\n \n try {\n const now = Date.now();\n \n if (!this.messages.has(key)) {\n this.messages.set(key, []);\n }\n \n const timestamps = this.messages.get(key);\n \n const validTimestamps = timestamps.filter(ts => now - ts < windowMs);\n \n if (validTimestamps.length >= limit) {\n return false; \n }\n \n validTimestamps.push(now);\n this.messages.set(key, validTimestamps);\n return true;\n } finally {\n this.locks.delete(key);\n }\n },\n \n async checkConnectionRate(identifier, limit = 5, windowMs = 300000) {\n if (typeof identifier !== 'string' || identifier.length > 256) {\n return false;\n }\n \n const key = `conn_${identifier}`;\n \n if (this.locks.has(key)) {\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 10) + 5));\n return this.checkConnectionRate(identifier, limit, windowMs);\n }\n \n this.locks.set(key, true);\n \n try {\n const now = Date.now();\n \n if (!this.connections.has(key)) {\n this.connections.set(key, []);\n }\n \n const timestamps = this.connections.get(key);\n const validTimestamps = timestamps.filter(ts => now - ts < windowMs);\n \n if (validTimestamps.length >= limit) {\n return false;\n }\n \n validTimestamps.push(now);\n this.connections.set(key, validTimestamps);\n return true;\n } finally {\n this.locks.delete(key);\n }\n },\n \n cleanup() {\n const now = Date.now();\n const maxAge = 3600000; \n \n for (const [key, timestamps] of this.messages.entries()) {\n if (this.locks.has(key)) continue;\n \n const valid = timestamps.filter(ts => now - ts < maxAge);\n if (valid.length === 0) {\n this.messages.delete(key);\n } else {\n this.messages.set(key, valid);\n }\n }\n \n for (const [key, timestamps] of this.connections.entries()) {\n if (this.locks.has(key)) continue;\n \n const valid = timestamps.filter(ts => now - ts < maxAge);\n if (valid.length === 0) {\n this.connections.delete(key);\n } else {\n this.connections.set(key, valid);\n }\n }\n\n for (const lockKey of this.locks.keys()) {\n const keyTimestamp = parseInt(lockKey.split('_').pop()) || 0;\n if (now - keyTimestamp > 30000) {\n this.locks.delete(lockKey);\n }\n }\n }\n};\n\n static validateSalt(salt) {\n if (!salt || salt.length !== 64) {\n throw new Error('Salt must be exactly 64 bytes');\n }\n \n const uniqueBytes = new Set(salt);\n if (uniqueBytes.size < 16) {\n throw new Error('Salt has insufficient entropy');\n }\n \n return true;\n }\n\n // Secure logging without data leaks\n static secureLog = {\n logs: [],\n maxLogs: 100,\n isProductionMode: false,\n \n // Initialize production mode detection\n init() {\n this.isProductionMode = this._detectProductionMode();\n if (this.isProductionMode) {\n console.log('[SecureChat] Production mode detected - sensitive logging disabled');\n }\n },\n \n _detectProductionMode() {\n return (\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') ||\n (!window.DEBUG_MODE && !window.DEVELOPMENT_MODE) ||\n (window.location.hostname && !window.location.hostname.includes('localhost') && \n !window.location.hostname.includes('127.0.0.1') && \n !window.location.hostname.includes('.local')) ||\n (typeof window.webpackHotUpdate === 'undefined' && !window.location.search.includes('debug'))\n );\n },\n \n log(level, message, context = {}) {\n const sanitizedContext = this.sanitizeContext(context);\n const logEntry = {\n timestamp: Date.now(),\n level,\n message,\n context: sanitizedContext,\n id: crypto.getRandomValues(new Uint32Array(1))[0]\n };\n \n this.logs.push(logEntry);\n \n // Keep only recent logs\n if (this.logs.length > this.maxLogs) {\n this.logs = this.logs.slice(-this.maxLogs);\n }\n \n // Production-safe console output\n if (this.isProductionMode) {\n if (level === 'error') {\n // In production expose only an opaque error code, never the context.\n console.error(`\u274C [SecureChat] ${message} [ERROR_CODE: ${this._generateErrorCode(message)}]`);\n } else if (level === 'warn') {\n // Warning text only, no context payload.\n console.warn(`\u26A0\uFE0F [SecureChat] ${message}`);\n } else {\n // info/debug and any other level: suppressed entirely in production.\n return;\n }\n } else {\n // Development mode - \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0432\u0441\u0435\n if (level === 'error') {\n console.error(`\u274C [SecureChat] ${message}`, { errorType: sanitizedContext?.constructor?.name || 'Unknown' });\n } else if (level === 'warn') {\n console.warn(`\u26A0\uFE0F [SecureChat] ${message}`, { details: sanitizedContext });\n } else {\n console.log(`[SecureChat] ${message}`, sanitizedContext);\n }\n }\n },\n \n // \u0413\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u043A\u043E\u0434 \u043E\u0448\u0438\u0431\u043A\u0438 \u0434\u043B\u044F production\n _generateErrorCode(message) {\n const hash = message.split('').reduce((a, b) => {\n a = ((a << 5) - a) + b.charCodeAt(0);\n return a & a;\n }, 0);\n return Math.abs(hash).toString(36).substring(0, 6).toUpperCase();\n },\n \n sanitizeContext(context) {\n if (!context || typeof context !== 'object') {\n return context;\n }\n \n const sensitivePatterns = [\n /key/i, /secret/i, /password/i, /token/i, /signature/i,\n /challenge/i, /proof/i, /salt/i, /iv/i, /nonce/i, /hash/i,\n /fingerprint/i, /mac/i, /private/i, /encryption/i, /decryption/i\n ];\n \n const sanitized = {};\n for (const [key, value] of Object.entries(context)) {\n const isSensitive = sensitivePatterns.some(pattern => \n pattern.test(key) || (typeof value === 'string' && pattern.test(value))\n );\n \n if (isSensitive) {\n sanitized[key] = '[REDACTED]';\n } else if (typeof value === 'string' && value.length > 100) {\n sanitized[key] = value.substring(0, 100) + '...[TRUNCATED]';\n } else if (value instanceof ArrayBuffer || value instanceof Uint8Array) {\n sanitized[key] = `[${value.constructor.name}(${value.byteLength || value.length} bytes)]`;\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\n // \u0420\u0435\u043A\u0443\u0440\u0441\u0438\u0432\u043D\u0430\u044F \u0441\u0430\u043D\u0438\u0442\u0438\u0437\u0430\u0446\u0438\u044F \u0434\u043B\u044F \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u0432\n sanitized[key] = this.sanitizeContext(value);\n } else {\n sanitized[key] = value;\n }\n }\n return sanitized;\n },\n \n getLogs(level = null) {\n if (level) {\n return this.logs.filter(log => log.level === level);\n }\n return [...this.logs];\n },\n \n clearLogs() {\n this.logs = [];\n },\n \n // \u041C\u0435\u0442\u043E\u0434 \u0434\u043B\u044F \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u043E\u0448\u0438\u0431\u043E\u043A \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432 production\n async sendErrorToServer(errorCode, message, context = {}) {\n if (!this.isProductionMode) {\n return; // \u0412 development \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C\n }\n \n try {\n // \u041E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u0443\u044E \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044E\n const safeErrorData = {\n errorCode,\n timestamp: Date.now(),\n userAgent: navigator.userAgent.substring(0, 100),\n url: window.location.href.substring(0, 100)\n };\n \n // \u0417\u0434\u0435\u0441\u044C \u043C\u043E\u0436\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\n // await fetch('/api/error-log', { method: 'POST', body: JSON.stringify(safeErrorData) });\n \n if (window.DEBUG_MODE) {\n console.log('[SecureChat] Error logged to server:', safeErrorData);\n }\n } catch (e) {\n // \u041D\u0435 \u043B\u043E\u0433\u0438\u0440\u0443\u0435\u043C \u043E\u0448\u0438\u0431\u043A\u0438 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F\n }\n }\n };\n\n // Generate ECDH key pair for secure key exchange (non-extractable) with fallback\n static async generateECDHKeyPair() {\n try {\n // Try P-384 first\n try {\n const keyPair = await crypto.subtle.generateKey(\n {\n name: 'ECDH',\n namedCurve: 'P-384'\n },\n false, // Non-extractable for enhanced security\n // 'deriveBits' is REQUIRED: deriveSharedKeys() uses deriveBits so\n // the shared secret lands in a buffer we can overwrite, instead of\n // being exported out of an extractable key and left in the heap.\n // Without this usage WebCrypto rejects the derivation outright and\n // no session can be established. Usages are local to the CryptoKey\n // and are not part of the exported SPKI, so this does not change\n // anything on the wire.\n ['deriveKey', 'deriveBits']\n );\n\n // Removed key generation info logging to avoid exposing key-related metadata\n\n return keyPair;\n } catch (p384Error) {\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 generation failed, switching curve', { error: p384Error.message });\n\n // Fallback to P-256\n const keyPair = await crypto.subtle.generateKey(\n {\n name: 'ECDH',\n namedCurve: 'P-256'\n },\n false, // Non-extractable for enhanced security\n ['deriveKey', 'deriveBits']\n );\n \n // Removed key generation info logging to avoid exposing key-related metadata\n \n return keyPair;\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH key generation failed', { error: error.message });\n throw new Error('Failed to create keys for secure exchange');\n }\n }\n\n // Generate ECDSA key pair for digital signatures with fallback\n static async generateECDSAKeyPair() {\n try {\n // Try P-384 first\n try {\n const keyPair = await crypto.subtle.generateKey(\n {\n name: 'ECDSA',\n namedCurve: 'P-384'\n },\n false, // Non-extractable for enhanced security\n ['sign', 'verify']\n );\n \n // Removed key generation info logging to avoid exposing key-related metadata\n \n return keyPair;\n } catch (p384Error) {\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 generation failed, switching curve', { error: p384Error.message });\n \n // Fallback to P-256\n const keyPair = await crypto.subtle.generateKey(\n {\n name: 'ECDSA',\n namedCurve: 'P-256'\n },\n false, // Non-extractable for enhanced security\n ['sign', 'verify']\n );\n \n // Removed key generation info logging to avoid exposing key-related metadata\n \n return keyPair;\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDSA key generation failed', { error: error.message });\n throw new Error('Failed to generate keys for digital signatures');\n }\n }\n\n // Sign data with ECDSA (P-384 or P-256)\n static async signData(privateKey, data) {\n try {\n const encoder = new TextEncoder();\n const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;\n \n // Try SHA-384 first, fallback to SHA-256\n try {\n const signature = await crypto.subtle.sign(\n {\n name: 'ECDSA',\n hash: 'SHA-384'\n },\n privateKey,\n dataBuffer\n );\n \n return Array.from(new Uint8Array(signature));\n } catch (sha384Error) {\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 signing failed, trying SHA-256', { error: sha384Error.message });\n \n const signature = await crypto.subtle.sign(\n {\n name: 'ECDSA',\n hash: 'SHA-256'\n },\n privateKey,\n dataBuffer\n );\n \n return Array.from(new Uint8Array(signature));\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Data signing failed', { error: error.message });\n throw new Error('Failed to sign data');\n }\n }\n\n // Verify ECDSA signature (P-384 or P-256)\n static async verifySignature(publicKey, signature, data) {\n try {\n // Debug logs removed\n \n const encoder = new TextEncoder();\n const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;\n const signatureBuffer = new Uint8Array(signature);\n \n // Debug logs removed\n \n // Try SHA-384 first, fallback to SHA-256\n try {\n // Debug logs removed\n const isValid = await crypto.subtle.verify(\n {\n name: 'ECDSA',\n hash: 'SHA-384'\n },\n publicKey,\n signatureBuffer,\n dataBuffer\n );\n \n // Debug logs removed\n \n // Removed signature verification info logging\n \n return isValid;\n } catch (sha384Error) {\n // Debug logs removed\n // Removed signature verification transition logging\n \n // Debug logs removed\n const isValid = await crypto.subtle.verify(\n {\n name: 'ECDSA',\n hash: 'SHA-256'\n },\n publicKey,\n signatureBuffer,\n dataBuffer\n );\n \n // Debug logs removed\n \n // Removed signature verification info logging\n \n return isValid;\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signature verification failed', { error: error.message });\n throw new Error('Failed to verify digital signature');\n }\n }\n\n // Enhanced DER/SPKI validation with full ASN.1 parsing\n static async validateKeyStructure(keyData, expectedAlgorithm = 'ECDH') {\n try {\n if (!Array.isArray(keyData) || keyData.length === 0) {\n throw new Error('Invalid key data format');\n }\n\n const keyBytes = new Uint8Array(keyData);\n\n // Size limits to prevent DoS\n if (keyBytes.length < 50) {\n throw new Error('Key data too short - invalid SPKI structure');\n }\n if (keyBytes.length > 2000) {\n throw new Error('Key data too long - possible attack');\n }\n\n // Parse ASN.1 DER structure\n const asn1 = EnhancedSecureCryptoUtils.parseASN1(keyBytes);\n \n // Validate SPKI structure\n if (!asn1 || asn1.tag !== 0x30) {\n throw new Error('Invalid SPKI structure - missing SEQUENCE tag');\n }\n\n // SPKI should have exactly 2 elements: AlgorithmIdentifier and BIT STRING\n if (asn1.children.length !== 2) {\n throw new Error(`Invalid SPKI structure - expected 2 elements, got ${asn1.children.length}`);\n }\n\n // Validate AlgorithmIdentifier\n const algIdentifier = asn1.children[0];\n if (algIdentifier.tag !== 0x30) {\n throw new Error('Invalid AlgorithmIdentifier - not a SEQUENCE');\n }\n\n // Parse algorithm OID\n const algOid = algIdentifier.children[0];\n if (algOid.tag !== 0x06) {\n throw new Error('Invalid algorithm OID - not an OBJECT IDENTIFIER');\n }\n\n // Validate algorithm OID based on expected algorithm\n const oidBytes = algOid.value;\n const oidString = EnhancedSecureCryptoUtils.oidToString(oidBytes);\n \n // Check for expected algorithms\n const validAlgorithms = {\n 'ECDH': ['1.2.840.10045.2.1'], // id-ecPublicKey\n 'ECDSA': ['1.2.840.10045.2.1'], // id-ecPublicKey (same as ECDH)\n 'RSA': ['1.2.840.113549.1.1.1'], // rsaEncryption\n 'AES-GCM': ['2.16.840.1.101.3.4.1.6', '2.16.840.1.101.3.4.1.46'] // AES-128-GCM, AES-256-GCM\n };\n\n const expectedOids = validAlgorithms[expectedAlgorithm];\n if (!expectedOids) {\n throw new Error(`Unknown algorithm: ${expectedAlgorithm}`);\n }\n\n if (!expectedOids.includes(oidString)) {\n throw new Error(`Invalid algorithm OID: expected ${expectedOids.join(' or ')}, got ${oidString}`);\n }\n\n // For EC algorithms, validate curve parameters\n if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {\n if (algIdentifier.children.length < 2) {\n throw new Error('Missing curve parameters for EC key');\n }\n\n const curveOid = algIdentifier.children[1];\n if (curveOid.tag !== 0x06) {\n throw new Error('Invalid curve OID - not an OBJECT IDENTIFIER');\n }\n\n const curveOidString = EnhancedSecureCryptoUtils.oidToString(curveOid.value);\n \n // Only allow P-256 and P-384 curves\n const validCurves = {\n '1.2.840.10045.3.1.7': 'P-256', // secp256r1\n '1.3.132.0.34': 'P-384' // secp384r1\n };\n\n if (!validCurves[curveOidString]) {\n throw new Error(`Invalid or unsupported curve OID: ${curveOidString}`);\n }\n\n // Removed curve validation info logging\n }\n\n // Validate public key BIT STRING\n const publicKeyBitString = asn1.children[1];\n if (publicKeyBitString.tag !== 0x03) {\n throw new Error('Invalid public key - not a BIT STRING');\n }\n\n // Check for unused bits (should be 0 for public keys)\n if (publicKeyBitString.value[0] !== 0x00) {\n throw new Error(`Invalid BIT STRING - unexpected unused bits: ${publicKeyBitString.value[0]}`);\n }\n\n // For EC keys, validate point format\n if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {\n const pointData = publicKeyBitString.value.slice(1); // Skip unused bits byte\n \n // Check for uncompressed point format (0x04)\n if (pointData[0] !== 0x04) {\n throw new Error(`Invalid EC point format: expected uncompressed (0x04), got 0x${pointData[0].toString(16)}`);\n }\n\n // Validate point size based on curve\n const expectedSizes = {\n 'P-256': 65, // 1 + 32 + 32\n 'P-384': 97 // 1 + 48 + 48\n };\n\n // We already validated the curve above, so we can determine expected size\n const curveOidString = EnhancedSecureCryptoUtils.oidToString(algIdentifier.children[1].value);\n const curveName = curveOidString === '1.2.840.10045.3.1.7' ? 'P-256' : 'P-384';\n const expectedSize = expectedSizes[curveName];\n\n if (pointData.length !== expectedSize) {\n throw new Error(`Invalid EC point size for ${curveName}: expected ${expectedSize}, got ${pointData.length}`);\n }\n }\n\n // Additional validation: try to import the key\n try {\n const algorithm = expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH'\n ? { name: expectedAlgorithm, namedCurve: 'P-384' }\n : { name: expectedAlgorithm };\n\n const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];\n \n await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);\n } catch (importError) {\n // Try P-256 as fallback for EC keys\n if (expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH') {\n try {\n const algorithm = { name: expectedAlgorithm, namedCurve: 'P-256' };\n const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];\n await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);\n } catch (fallbackError) {\n throw new Error(`Key import validation failed: ${fallbackError.message}`);\n }\n } else {\n throw new Error(`Key import validation failed: ${importError.message}`);\n }\n }\n\n // Removed key structure validation info logging\n\n return true;\n } catch (err) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Key structure validation failed', {\n error: err.message,\n algorithm: expectedAlgorithm\n });\n throw new Error(`Invalid key structure: ${err.message}`);\n }\n }\n\n // ASN.1 DER parser helper\n static parseASN1(bytes, offset = 0) {\n if (offset >= bytes.length) {\n return null;\n }\n\n const tag = bytes[offset];\n let lengthOffset = offset + 1;\n \n if (lengthOffset >= bytes.length) {\n throw new Error('Truncated ASN.1 structure');\n }\n\n let length = bytes[lengthOffset];\n let valueOffset = lengthOffset + 1;\n\n // Handle long form length\n if (length & 0x80) {\n const numLengthBytes = length & 0x7f;\n if (numLengthBytes > 4) {\n throw new Error('ASN.1 length too large');\n }\n \n length = 0;\n for (let i = 0; i < numLengthBytes; i++) {\n if (valueOffset + i >= bytes.length) {\n throw new Error('Truncated ASN.1 length');\n }\n length = (length << 8) | bytes[valueOffset + i];\n }\n valueOffset += numLengthBytes;\n }\n\n if (valueOffset + length > bytes.length) {\n throw new Error('ASN.1 structure extends beyond data');\n }\n\n const value = bytes.slice(valueOffset, valueOffset + length);\n const node = {\n tag: tag,\n length: length,\n value: value,\n children: []\n };\n\n // Parse children for SEQUENCE and SET\n if (tag === 0x30 || tag === 0x31) {\n let childOffset = 0;\n while (childOffset < value.length) {\n const child = EnhancedSecureCryptoUtils.parseASN1(value, childOffset);\n if (!child) break;\n node.children.push(child);\n childOffset = childOffset + 1 + child.lengthBytes + child.length;\n }\n }\n\n // Calculate how many bytes were used for length encoding\n node.lengthBytes = valueOffset - lengthOffset;\n \n return node;\n }\n\n // OID decoder helper\n static oidToString(bytes) {\n if (!bytes || bytes.length === 0) {\n throw new Error('Empty OID');\n }\n\n const parts = [];\n \n // First byte encodes first two components\n const first = Math.floor(bytes[0] / 40);\n const second = bytes[0] % 40;\n parts.push(first);\n parts.push(second);\n\n // Decode remaining components\n let value = 0;\n for (let i = 1; i < bytes.length; i++) {\n value = (value << 7) | (bytes[i] & 0x7f);\n if (!(bytes[i] & 0x80)) {\n parts.push(value);\n value = 0;\n }\n }\n\n return parts.join('.');\n }\n\n // Helper to validate and sanitize OID string\n static validateOidString(oidString) {\n // OID format: digits separated by dots\n const oidRegex = /^[0-9]+(\\.[0-9]+)*$/;\n if (!oidRegex.test(oidString)) {\n throw new Error(`Invalid OID format: ${oidString}`);\n }\n\n const parts = oidString.split('.').map(Number);\n \n // First component must be 0, 1, or 2\n if (parts[0] > 2) {\n throw new Error(`Invalid OID first component: ${parts[0]}`);\n }\n\n // If first component is 0 or 1, second must be <= 39\n if ((parts[0] === 0 || parts[0] === 1) && parts[1] > 39) {\n throw new Error(`Invalid OID second component: ${parts[1]} (must be <= 39 for first component ${parts[0]})`);\n }\n\n return true;\n }\n\n // Export public key for transmission with signature \n static async exportPublicKeyWithSignature(publicKey, signingKey, keyType = 'ECDH') {\n try {\n // Validate key type\n if (!['ECDH', 'ECDSA'].includes(keyType)) {\n throw new Error('Invalid key type');\n }\n \n const exported = await crypto.subtle.exportKey('spki', publicKey);\n const keyData = Array.from(new Uint8Array(exported));\n \n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);\n \n // Create signed key package\n const keyPackage = {\n keyType,\n keyData,\n timestamp: Date.now(),\n version: '4.0'\n };\n \n // Sign the key package\n const packageString = JSON.stringify(keyPackage);\n const signature = await EnhancedSecureCryptoUtils.signData(signingKey, packageString);\n \n const signedPackage = {\n ...keyPackage,\n signature\n };\n \n // Removed public key export with signature info logging\n \n return signedPackage;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key export failed', {\n error: error.message,\n keyType\n });\n throw new Error(`Failed to export ${keyType} key: ${error.message}`);\n }\n }\n\n // Import and verify signed public key\n static async importSignedPublicKey(signedPackage, verifyingKey, expectedKeyType = 'ECDH') {\n try {\n // Debug logs removed\n \n // Validate package structure\n if (!signedPackage || typeof signedPackage !== 'object') {\n throw new Error('Invalid signed package format');\n }\n \n const { keyType, keyData, timestamp, version, signature } = signedPackage;\n \n if (!keyType || !keyData || !timestamp || !signature) {\n throw new Error('Missing required fields in signed package');\n }\n \n if (!EnhancedSecureCryptoUtils.constantTimeCompare(keyType, expectedKeyType)) {\n throw new Error(`Key type mismatch: expected ${expectedKeyType}, got ${keyType}`);\n }\n \n // Check timestamp (reject keys older than 1 hour)\n const keyAge = Date.now() - timestamp;\n if (keyAge > 3600000) {\n throw new Error('Signed key package is too old');\n }\n \n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);\n \n // Verify signature\n const packageCopy = { keyType, keyData, timestamp, version };\n const packageString = JSON.stringify(packageCopy);\n // Debug logs removed\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signature, packageString);\n // Debug logs removed\n \n if (!isValidSignature) {\n throw new Error('Invalid signature on key package - possible MITM attack');\n }\n \n // Import the key with fallback support\n const keyBytes = new Uint8Array(keyData);\n \n // Try P-384 first\n try {\n const algorithm = keyType === 'ECDH' ?\n { name: 'ECDH', namedCurve: 'P-384' }\n : { name: 'ECDSA', namedCurve: 'P-384' };\n \n const keyUsages = keyType === 'ECDH' ? [] : ['verify'];\n \n const publicKey = await crypto.subtle.importKey(\n 'spki',\n keyBytes,\n algorithm,\n false, // Non-extractable\n keyUsages\n );\n \n // Removed public key import info logging\n \n return publicKey;\n } catch (p384Error) {\n // Fallback to P-256\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 import failed, switching curve', { error: p384Error.message });\n \n const algorithm = keyType === 'ECDH' ?\n { name: 'ECDH', namedCurve: 'P-256' }\n : { name: 'ECDSA', namedCurve: 'P-256' };\n \n const keyUsages = keyType === 'ECDH' ? [] : ['verify'];\n \n const publicKey = await crypto.subtle.importKey(\n 'spki',\n keyBytes,\n algorithm,\n false, // Non-extractable\n keyUsages\n );\n \n // Removed public key import info logging\n \n return publicKey;\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed public key import failed', {\n error: error.message,\n expectedKeyType\n });\n throw new Error(`Failed to import the signed key: ${error.message}`);\n }\n }\n\n // Legacy export for backward compatibility\n static async exportPublicKey(publicKey) {\n try {\n const exported = await crypto.subtle.exportKey('spki', publicKey);\n const keyData = Array.from(new Uint8Array(exported));\n \n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');\n \n // Removed legacy public key export info logging\n return keyData;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key export failed', { error: error.message });\n throw new Error('Failed to export the public key');\n }\n }\n\n // Legacy import for backward compatibility with fallback\n static async importPublicKey(keyData) {\n try {\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');\n \n const keyBytes = new Uint8Array(keyData);\n \n // Try P-384 first\n try {\n const publicKey = await crypto.subtle.importKey(\n 'spki',\n keyBytes,\n {\n name: 'ECDH',\n namedCurve: 'P-384'\n },\n false, // Non-extractable\n []\n );\n \n // Removed legacy public key import info logging\n return publicKey;\n } catch (p384Error) {\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });\n \n // Fallback to P-256\n const publicKey = await crypto.subtle.importKey(\n 'spki',\n keyBytes,\n {\n name: 'ECDH',\n namedCurve: 'P-256'\n },\n false, // Non-extractable\n []\n );\n \n // Removed legacy public key import info logging\n return publicKey;\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key import failed', { error: error.message });\n throw new Error('Failed to import the public key');\n }\n }\n\n\n // Method to check if a key is trusted\n static isKeyTrusted(keyOrFingerprint) {\n if (keyOrFingerprint instanceof CryptoKey) {\n const meta = EnhancedSecureCryptoUtils._keyMetadata.get(keyOrFingerprint);\n return meta ? meta.trusted === true : false;\n } else if (keyOrFingerprint && keyOrFingerprint._securityMetadata) {\n // Check by key metadata\n return keyOrFingerprint._securityMetadata.trusted === true;\n }\n\n return false;\n }\n\n static async importPublicKeyFromSignedPackage(signedPackage, verifyingKey = null, options = {}) {\n try {\n if (!signedPackage || !signedPackage.keyData || !signedPackage.signature) {\n throw new Error('Invalid signed key package format');\n }\n\n // Validate all required fields are present\n const requiredFields = ['keyData', 'signature', 'keyType', 'timestamp', 'version'];\n const missingFields = requiredFields.filter(field => !signedPackage[field]);\n\n if (missingFields.length > 0) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Missing required fields in signed package', {\n missingFields: missingFields,\n availableFields: Object.keys(signedPackage)\n });\n throw new Error(`Required fields are missing in the signed package: ${missingFields.join(', ')}`);\n }\n\n // SECURITY ENHANCEMENT: MANDATORY signature verification for signed packages\n if (!verifyingKey) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY VIOLATION: Signed package received without verifying key', {\n keyType: signedPackage.keyType,\n keySize: signedPackage.keyData.length,\n timestamp: signedPackage.timestamp,\n version: signedPackage.version,\n securityRisk: 'HIGH - Potential MITM attack vector'\n });\n\n // REJECT the signed package if no verifying key provided\n throw new Error('CRITICAL SECURITY ERROR: Signed key package received without a verification key. ' +\n 'This may indicate a possible MITM attack attempt. Import rejected for security reasons.');\n }\n\n // \u041E\u0411\u041D\u041E\u0412\u041B\u0415\u041D\u041E: \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u043D\u0443\u044E \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044E\n await EnhancedSecureCryptoUtils.validateKeyStructure(signedPackage.keyData, signedPackage.keyType || 'ECDH');\n\n // MANDATORY signature verification when verifyingKey is provided\n const packageCopy = { ...signedPackage };\n delete packageCopy.signature;\n const packageString = JSON.stringify(packageCopy);\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signedPackage.signature, packageString);\n\n if (!isValidSignature) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY BREACH: Invalid signature detected - MITM attack prevented', {\n keyType: signedPackage.keyType,\n keySize: signedPackage.keyData.length,\n timestamp: signedPackage.timestamp,\n version: signedPackage.version,\n attackPrevented: true\n });\n throw new Error('CRITICAL SECURITY ERROR: Invalid key signature detected. ' +\n 'This indicates a possible MITM attack attempt. Key import rejected.');\n }\n\n // Additional MITM protection: Check for key reuse and suspicious patterns\n const keyFingerprint = await EnhancedSecureCryptoUtils.calculateKeyFingerprint(signedPackage.keyData);\n\n // Log successful verification with security details\n // Removed signature verification pass details to avoid key-related logging\n\n // Import the public key with fallback\n const keyBytes = new Uint8Array(signedPackage.keyData);\n const keyType = signedPackage.keyType || 'ECDH';\n\n // Try P-384 first\n try {\n const publicKey = await crypto.subtle.importKey(\n 'spki',\n keyBytes,\n {\n name: keyType,\n namedCurve: 'P-384'\n },\n false, // Non-extractable\n keyType === 'ECDSA' ? ['verify'] : []\n );\n\n // Use WeakMap to store metadata\n EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, {\n trusted: true,\n verificationStatus: 'VERIFIED_SECURE',\n verificationTimestamp: Date.now()\n });\n\n return publicKey;\n } catch (p384Error) {\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });\n\n // Fallback to P-256\n const publicKey = await crypto.subtle.importKey(\n 'spki',\n keyBytes,\n {\n name: keyType,\n namedCurve: 'P-256'\n },\n false, // Non-extractable\n keyType === 'ECDSA' ? ['verify'] : []\n );\n\n // Use WeakMap to store metadata\n EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, {\n trusted: true,\n verificationStatus: 'VERIFIED_SECURE',\n verificationTimestamp: Date.now()\n });\n\n return publicKey;\n }\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed package key import failed', {\n error: error.message,\n securityImplications: 'Potential security breach prevented'\n });\n throw new Error(`Failed to import the public key from the signed package: ${error.message}`);\n }\n }\n\n // Enhanced key derivation with metadata protection and 64-byte salt\n static async deriveSharedKeys(privateKey, publicKey, salt) {\n try {\n // Removed detailed key derivation logging\n \n // Validate input parameters are CryptoKey instances\n if (!(privateKey instanceof CryptoKey)) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Private key is not a CryptoKey', {\n privateKeyType: typeof privateKey,\n privateKeyAlgorithm: privateKey?.algorithm?.name\n });\n throw new Error('The private key is not a valid CryptoKey.');\n }\n \n if (!(publicKey instanceof CryptoKey)) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key is not a CryptoKey', {\n publicKeyType: typeof publicKey,\n publicKeyAlgorithm: publicKey?.algorithm?.name\n });\n throw new Error('The public key is not a valid CryptoKey.');\n }\n \n // Validate salt size (should be 64 bytes for enhanced security)\n if (!salt || salt.length !== 64) {\n throw new Error('Salt must be exactly 64 bytes for enhanced security');\n }\n \n const saltBytes = new Uint8Array(salt);\n const encoder = new TextEncoder();\n \n // Step 1: Derive the raw ECDH shared secret as HKDF input material.\n //\n // This used to derive an EXTRACTABLE AES-GCM key and then exportKey()\n // it, which put the shared secret into an ArrayBuffer that was never\n // cleared \u2014 it simply fell out of scope and sat in the JS heap until\n // GC, readable by anything with access to the page (a compromised\n // extension, a heap snapshot in a crash report). Every session key is\n // derived from those 32 bytes with public salt and hard-coded info\n // strings, so recovering them recovers the whole session.\n //\n // deriveBits gives the same bytes without the detour through an\n // extractable CryptoKey, and hands back a buffer we own and can wipe.\n // WIRE COMPATIBILITY: for ECDH, deriveBits(n) returns the leftmost n\n // bits of the shared X coordinate, which is exactly what deriveKey to\n // AES-GCM-256 used \u2014 so 256 here reproduces the previous bytes exactly\n // and a 5.6.1 client still interoperates with 5.6.0. Do not \"improve\"\n // this to 384 without a protocol version bump.\n let rawSharedSecret;\n let sharedSecretBits = null;\n try {\n sharedSecretBits = await crypto.subtle.deriveBits(\n {\n name: 'ECDH',\n public: publicKey\n },\n privateKey,\n 256\n );\n\n rawSharedSecret = await crypto.subtle.importKey(\n 'raw',\n sharedSecretBits,\n {\n name: 'HKDF',\n hash: 'SHA-256'\n },\n false,\n // deriveBits is required for the fingerprint material below;\n // without it that call fails with an InvalidAccessError.\n ['deriveKey', 'deriveBits']\n );\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH derivation failed', {\n error: error.message\n });\n throw error;\n } finally {\n // importKey copies the material, so the source buffer is dead\n // weight from here on \u2014 overwrite it rather than leaving the\n // shared secret lying in the heap.\n if (sharedSecretBits) {\n EnhancedSecureCryptoUtils.zeroizeBuffer(sharedSecretBits);\n sharedSecretBits = null;\n }\n }\n\n // Step 2: Use HKDF to derive specific keys directly\n // Removed detailed key derivation logging\n\n // Step 3: Derive specific keys using HKDF with unique info parameters\n // Each key uses unique info parameter for proper separation\n \n // Derive message encryption key (messageKey)\n let messageKey;\n messageKey = await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: saltBytes,\n info: encoder.encode('message-encryption-v4')\n },\n rawSharedSecret,\n {\n name: 'AES-GCM',\n length: 256\n },\n false, // Non-extractable for enhanced security\n ['encrypt', 'decrypt']\n );\n\n // Derive MAC key for message authentication\n let macKey;\n macKey = await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: saltBytes,\n info: encoder.encode('message-authentication-v4')\n },\n rawSharedSecret,\n {\n name: 'HMAC',\n hash: 'SHA-256'\n },\n false, // Non-extractable\n ['sign', 'verify']\n );\n\n // Derive Perfect Forward Secrecy key (pfsKey)\n let pfsKey;\n pfsKey = await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: saltBytes,\n info: encoder.encode('perfect-forward-secrecy-v4')\n },\n rawSharedSecret,\n {\n name: 'AES-GCM',\n length: 256\n },\n false, // Non-extractable\n ['encrypt', 'decrypt']\n );\n\n // Derive separate metadata encryption key\n let metadataKey;\n metadataKey = await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: saltBytes,\n info: encoder.encode('metadata-protection-v4')\n },\n rawSharedSecret,\n {\n name: 'AES-GCM',\n length: 256\n },\n false, // Non-extractable\n ['encrypt', 'decrypt']\n );\n\n // Root key for the Double Ratchet, derived here rather than handing the\n // raw ECDH secret to the caller: the secret is wiped before this\n // function returns (see the finally above), and only this 32-byte\n // branch of the KDF tree ever leaves. Its own info string keeps it\n // domain-separated from the message, MAC and metadata keys, so\n // learning a session key tells an attacker nothing about the ratchet.\n const ratchetRootBits = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: saltBytes,\n info: encoder.encode('double-ratchet-root-v1')\n },\n rawSharedSecret,\n 256\n );\n const ratchetRoot = new Uint8Array(ratchetRootBits);\n\n // Fingerprint material. Previously this derived a second EXTRACTABLE\n // AES key purely so it could be exported \u2014 leaving another copy of\n // key-derived material in the heap with nothing wiping it. HKDF can\n // hand back raw bits directly; same salt, same info, same 256 bits, so\n // the fingerprint (and therefore the SAS built on it) is unchanged.\n let fingerprintBits = null;\n let fingerprint;\n try {\n fingerprintBits = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: saltBytes,\n info: encoder.encode('fingerprint-generation-v4')\n },\n rawSharedSecret,\n 256\n );\n // A Uint8Array view, not Array.from(): the array copy was a third\n // copy of key-derived bytes in the heap that nothing cleared.\n fingerprint = await EnhancedSecureCryptoUtils.generateKeyFingerprint(\n new Uint8Array(fingerprintBits)\n );\n } finally {\n if (fingerprintBits) {\n EnhancedSecureCryptoUtils.zeroizeBuffer(fingerprintBits);\n fingerprintBits = null;\n }\n }\n\n // Validate that all derived keys are CryptoKey instances\n if (!(messageKey instanceof CryptoKey)) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived message key is not a CryptoKey', {\n messageKeyType: typeof messageKey,\n messageKeyAlgorithm: messageKey?.algorithm?.name\n });\n throw new Error('The derived message key is not a valid CryptoKey.');\n }\n \n if (!(macKey instanceof CryptoKey)) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived MAC key is not a CryptoKey', {\n macKeyType: typeof macKey,\n macKeyAlgorithm: macKey?.algorithm?.name\n });\n throw new Error('The derived MAC key is not a valid CryptoKey.');\n }\n \n if (!(pfsKey instanceof CryptoKey)) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived PFS key is not a CryptoKey', {\n pfsKeyType: typeof pfsKey,\n pfsKeyAlgorithm: pfsKey?.algorithm?.name\n });\n throw new Error('The derived PFS key is not a valid CryptoKey.');\n }\n \n if (!(metadataKey instanceof CryptoKey)) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived metadata key is not a CryptoKey', {\n metadataKeyType: typeof metadataKey,\n metadataKeyAlgorithm: metadataKey?.algorithm?.name\n });\n throw new Error('The derived metadata key is not a valid CryptoKey.');\n }\n\n // Removed detailed key derivation success logging\n\n return {\n messageKey, // Renamed from encryptionKey for clarity\n macKey,\n pfsKey, // Added Perfect Forward Secrecy key\n metadataKey,\n // Raw bytes on purpose: a ratchet has to chain KDFs itself, which\n // WebCrypto cannot do behind a non-extractable handle. The caller\n // must hand this to DoubleRatchet.init() and zeroize it.\n ratchetRoot,\n fingerprint,\n timestamp: Date.now(),\n version: '4.0'\n };\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced key derivation failed', { \n error: error.message,\n errorStack: error.stack,\n privateKeyType: typeof privateKey,\n publicKeyType: typeof publicKey,\n saltLength: salt?.length,\n privateKeyAlgorithm: privateKey?.algorithm?.name,\n publicKeyAlgorithm: publicKey?.algorithm?.name\n });\n throw new Error(`Failed to create shared encryption keys: ${error.message}`);\n }\n }\n\n static async generateKeyFingerprint(keyData) {\n const keyBuffer = new Uint8Array(keyData);\n const hashBuffer = await crypto.subtle.digest('SHA-384', keyBuffer);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n return hashArray.slice(0, 12).map(b => b.toString(16).padStart(2, '0')).join(':');\n }\n\n // Generate mutual authentication challenge\n static generateMutualAuthChallenge() {\n const challenge = crypto.getRandomValues(new Uint8Array(48)); // Increased to 48 bytes\n const timestamp = Date.now();\n const nonce = crypto.getRandomValues(new Uint8Array(16));\n \n return {\n challenge: Array.from(challenge),\n timestamp,\n nonce: Array.from(nonce),\n version: '4.0'\n };\n }\n\n // Create cryptographic proof for mutual authentication\n static async createAuthProof(challenge, privateKey, publicKey) {\n try {\n if (!challenge || !challenge.challenge || !challenge.timestamp || !challenge.nonce) {\n throw new Error('Invalid challenge structure');\n }\n \n // Check challenge age (max 2 minutes)\n const challengeAge = Date.now() - challenge.timestamp;\n if (challengeAge > 120000) {\n throw new Error('Challenge expired');\n }\n \n // Create proof data\n const proofData = {\n challenge: challenge.challenge,\n timestamp: challenge.timestamp,\n nonce: challenge.nonce,\n responseTimestamp: Date.now(),\n publicKeyHash: await EnhancedSecureCryptoUtils.hashPublicKey(publicKey)\n };\n \n // Sign the proof\n const proofString = JSON.stringify(proofData);\n const signature = await EnhancedSecureCryptoUtils.signData(privateKey, proofString);\n \n const proof = {\n ...proofData,\n signature,\n version: '4.0'\n };\n \n EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof created', {\n challengeAge: Math.round(challengeAge / 1000) + 's'\n });\n \n return proof;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof creation failed', { error: error.message });\n throw new Error(`Failed to create cryptographic proof: ${error.message}`);\n }\n }\n\n // Verify mutual authentication proof\n static async verifyAuthProof(proof, challenge, publicKey) {\n try {\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 20) + 5));\n // Assert the public key is valid and has the correct usage\n EnhancedSecureCryptoUtils.assertCryptoKey(publicKey, 'ECDSA', ['verify']);\n\n if (!proof || !challenge || !publicKey) {\n throw new Error('Missing required parameters for proof verification');\n }\n\n // Validate proof structure\n const requiredFields = ['challenge', 'timestamp', 'nonce', 'responseTimestamp', 'publicKeyHash', 'signature'];\n for (const field of requiredFields) {\n if (!proof[field]) {\n throw new Error(`Missing required field: ${field}`);\n }\n }\n\n // Verify challenge matches\n if (!EnhancedSecureCryptoUtils.constantTimeCompareArrays(proof.challenge, challenge.challenge) ||\n proof.timestamp !== challenge.timestamp ||\n !EnhancedSecureCryptoUtils.constantTimeCompareArrays(proof.nonce, challenge.nonce)) {\n throw new Error('Challenge mismatch - possible replay attack');\n }\n\n // Check response time (max 30 minutes for better UX)\n const responseAge = Date.now() - proof.responseTimestamp;\n if (responseAge > 1800000) {\n throw new Error('Proof response expired');\n }\n\n // Verify public key hash\n const expectedHash = await EnhancedSecureCryptoUtils.hashPublicKey(publicKey);\n if (!EnhancedSecureCryptoUtils.constantTimeCompare(proof.publicKeyHash, expectedHash)) {\n throw new Error('Public key hash mismatch');\n }\n\n // Verify signature\n const proofCopy = { ...proof };\n delete proofCopy.signature;\n const proofString = JSON.stringify(proofCopy);\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(publicKey, proof.signature, proofString);\n\n if (!isValidSignature) {\n throw new Error('Invalid proof signature');\n }\n\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof verified successfully', {\n responseAge: Math.round(responseAge / 1000) + 's'\n });\n\n return true;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof verification failed', { error: error.message });\n throw new Error(`Failed to verify cryptographic proof: ${error.message}`);\n }\n }\n\n // Hash public key for verification\n static async hashPublicKey(publicKey) {\n try {\n const exported = await crypto.subtle.exportKey('spki', publicKey);\n const hash = await crypto.subtle.digest('SHA-384', exported);\n const hashArray = Array.from(new Uint8Array(hash));\n return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key hashing failed', { error: error.message });\n throw new Error('Failed to create hash of the public key');\n }\n }\n\n // Legacy authentication challenge for backward compatibility\n static generateAuthChallenge() {\n const challenge = crypto.getRandomValues(new Uint8Array(32));\n return Array.from(challenge);\n }\n\n // Generate verification code for out-of-band authentication\n static generateVerificationCode() {\n const chars = '0123456789ABCDEF';\n const charCount = chars.length;\n let result = '';\n \n // Use rejection sampling to avoid bias\n for (let i = 0; i < 6; i++) {\n let randomByte;\n do {\n randomByte = crypto.getRandomValues(new Uint8Array(1))[0];\n } while (randomByte >= 256 - (256 % charCount)); // Reject biased values\n \n result += chars[randomByte % charCount];\n }\n \n return result.match(/.{1,2}/g).join('-');\n }\n\n // Enhanced message encryption with metadata protection and sequence numbers\n static async encryptMessage(message, encryptionKey, macKey, metadataKey, messageId, sequenceNumber = 0) {\n try {\n if (!message || typeof message !== 'string') {\n throw new Error('Invalid message format');\n }\n\n EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['encrypt']);\n EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['sign']);\n EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['encrypt']);\n\n const encoder = new TextEncoder();\n const messageData = encoder.encode(message);\n const messageIv = crypto.getRandomValues(new Uint8Array(12));\n const metadataIv = crypto.getRandomValues(new Uint8Array(12));\n const timestamp = Date.now();\n\n const paddingSize = 16 - (messageData.length % 16);\n const paddedMessage = new Uint8Array(messageData.length + paddingSize);\n paddedMessage.set(messageData);\n const padding = crypto.getRandomValues(new Uint8Array(paddingSize));\n paddedMessage.set(padding, messageData.length);\n\n const encryptedMessage = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: messageIv },\n encryptionKey,\n paddedMessage\n );\n\n const metadata = {\n id: messageId,\n timestamp: timestamp,\n sequenceNumber: sequenceNumber,\n originalLength: messageData.length,\n version: '4.0'\n };\n\n const metadataStr = JSON.stringify(EnhancedSecureCryptoUtils.sortObjectKeys(metadata));\n const encryptedMetadata = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: metadataIv },\n metadataKey,\n encoder.encode(metadataStr)\n );\n\n const payload = {\n messageIv: Array.from(messageIv),\n messageData: Array.from(new Uint8Array(encryptedMessage)),\n metadataIv: Array.from(metadataIv),\n metadataData: Array.from(new Uint8Array(encryptedMetadata)),\n version: '4.0'\n };\n\n const sortedPayload = EnhancedSecureCryptoUtils.sortObjectKeys(payload);\n const payloadStr = JSON.stringify(sortedPayload);\n\n const mac = await crypto.subtle.sign(\n 'HMAC',\n macKey,\n encoder.encode(payloadStr)\n );\n\n payload.mac = Array.from(new Uint8Array(mac));\n\n // Logging removed to avoid noisy console output\n\n return payload;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Message encryption failed', {\n error: error.message,\n messageId\n });\n throw new Error(`Failed to encrypt the message: ${error.message}`);\n }\n }\n\n // Enhanced message decryption with metadata protection and sequence validation\n static async decryptMessage(encryptedPayload, encryptionKey, macKey, metadataKey, expectedSequenceNumber = null) {\n try {\n EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['decrypt']);\n EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['verify']);\n EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['decrypt']);\n\n const requiredFields = ['messageIv', 'messageData', 'metadataIv', 'metadataData', 'mac', 'version'];\n for (const field of requiredFields) {\n if (!encryptedPayload[field]) {\n throw new Error(`Missing required field: ${field}`);\n }\n }\n\n const payloadCopy = { ...encryptedPayload };\n delete payloadCopy.mac;\n const sortedPayloadCopy = EnhancedSecureCryptoUtils.sortObjectKeys(payloadCopy);\n const payloadStr = JSON.stringify(sortedPayloadCopy);\n\n const macValid = await crypto.subtle.verify(\n 'HMAC',\n macKey,\n new Uint8Array(encryptedPayload.mac),\n new TextEncoder().encode(payloadStr)\n );\n\n if (!macValid) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'MAC verification failed', {\n payloadFields: Object.keys(encryptedPayload),\n macLength: encryptedPayload.mac?.length\n });\n throw new Error('Message authentication failed - possible tampering');\n }\n\n const metadataIv = new Uint8Array(encryptedPayload.metadataIv);\n const metadataData = new Uint8Array(encryptedPayload.metadataData);\n\n const decryptedMetadataBuffer = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: metadataIv },\n metadataKey,\n metadataData\n );\n\n const metadataStr = new TextDecoder().decode(decryptedMetadataBuffer);\n const metadata = JSON.parse(metadataStr);\n\n if (!metadata.id || !metadata.timestamp || metadata.sequenceNumber === undefined || !metadata.originalLength) {\n throw new Error('Invalid metadata structure');\n }\n\n const messageAge = Date.now() - metadata.timestamp;\n if (messageAge > 1800000) { // 30 minutes for better UX\n throw new Error('Message expired (older than 30 minutes)');\n }\n\n if (expectedSequenceNumber !== null) {\n // A sequence number below what we expect means the frame is a\n // replay (or badly out of order on a channel that is ordered and\n // reliable). Downgrading that to a warning and decrypting anyway\n // defeats the purpose of tracking sequence numbers at all.\n if (metadata.sequenceNumber < expectedSequenceNumber) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Rejected message with stale sequence number - possible replay', {\n expected: expectedSequenceNumber,\n received: metadata.sequenceNumber,\n messageId: metadata.id\n });\n throw new Error(`Stale sequence number: expected at least ${expectedSequenceNumber}, got ${metadata.sequenceNumber}`);\n } else if (metadata.sequenceNumber > expectedSequenceNumber + 10) {\n throw new Error(`Sequence number gap too large: expected around ${expectedSequenceNumber}, got ${metadata.sequenceNumber}`);\n }\n }\n\n const messageIv = new Uint8Array(encryptedPayload.messageIv);\n const messageData = new Uint8Array(encryptedPayload.messageData);\n\n const decryptedMessageBuffer = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: messageIv },\n encryptionKey,\n messageData\n );\n\n const paddedMessage = new Uint8Array(decryptedMessageBuffer);\n const originalMessage = paddedMessage.slice(0, metadata.originalLength);\n\n const decoder = new TextDecoder();\n const message = decoder.decode(originalMessage);\n\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Message decrypted successfully', {\n messageId: metadata.id,\n sequenceNumber: metadata.sequenceNumber,\n messageAge: Math.round(messageAge / 1000) + 's'\n });\n\n return {\n message: message,\n messageId: metadata.id,\n timestamp: metadata.timestamp,\n sequenceNumber: metadata.sequenceNumber\n };\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Message decryption failed', { error: error.message });\n throw new Error(`Failed to decrypt the message: ${error.message}`);\n }\n }\n\n static _getMessageSanitizer() {\n if (EnhancedSecureCryptoUtils._messageSanitizer) {\n return EnhancedSecureCryptoUtils._messageSanitizer;\n }\n\n if (typeof window === 'undefined' || !window?.document) {\n throw new Error('DOMPurify requires a browser-like window for message sanitization');\n }\n\n EnhancedSecureCryptoUtils._messageSanitizer = createDOMPurify(window);\n return EnhancedSecureCryptoUtils._messageSanitizer;\n }\n\n // Centralized chat-message sanitization. Messages are rendered as plain text,\n // so the safest compatible output is text-only content with no markup surface.\n static sanitizeMessage(message) {\n if (typeof message !== 'string') {\n throw new Error('Message must be a string');\n }\n\n const sanitized = EnhancedSecureCryptoUtils._getMessageSanitizer().sanitize(message, {\n ALLOWED_TAGS: [],\n ALLOWED_ATTR: [],\n ALLOW_UNKNOWN_PROTOCOLS: false,\n FORBID_TAGS: ['script', 'style', 'svg', 'math', 'template'],\n FORBID_ATTR: ['style'],\n KEEP_CONTENT: true,\n RETURN_TRUSTED_TYPE: false,\n USE_PROFILES: {\n html: false,\n svg: false,\n svgFilters: false,\n mathMl: false\n }\n });\n\n return String(sanitized).trim().substring(0, 2000);\n }\n\n // Generate cryptographically secure salt (64 bytes for enhanced security)\n static generateSalt() {\n return Array.from(crypto.getRandomValues(new Uint8Array(64)));\n }\n\n // Calculate key fingerprint for MITM protection\n static async calculateKeyFingerprint(keyData) {\n try {\n const encoder = new TextEncoder();\n const keyBytes = new Uint8Array(keyData);\n \n // Create a hash of the key data for fingerprinting\n const hashBuffer = await crypto.subtle.digest('SHA-256', keyBytes);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n \n // Convert to hexadecimal string\n const fingerprint = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n \n // Removed key fingerprint logging\n \n return fingerprint;\n } catch (error) {\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Key fingerprint calculation failed', { error: error.message });\n throw new Error('Failed to compute the key fingerprint');\n }\n }\n\n static constantTimeCompare(a, b) {\n const strA = typeof a === 'string' ? a : JSON.stringify(a);\n const strB = typeof b === 'string' ? b : JSON.stringify(b);\n \n if (strA.length !== strB.length) {\n let dummy = 0;\n for (let i = 0; i < Math.max(strA.length, strB.length); i++) {\n dummy |= (strA.charCodeAt(i % strA.length) || 0) ^ (strB.charCodeAt(i % strB.length) || 0);\n }\n return false;\n }\n \n let result = 0;\n for (let i = 0; i < strA.length; i++) {\n result |= strA.charCodeAt(i) ^ strB.charCodeAt(i);\n }\n \n return result === 0;\n }\n\n static constantTimeCompareArrays(arr1, arr2) {\n if (!Array.isArray(arr1) || !Array.isArray(arr2)) {\n return false;\n }\n \n if (arr1.length !== arr2.length) {\n let dummy = 0;\n const maxLen = Math.max(arr1.length, arr2.length);\n for (let i = 0; i < maxLen; i++) {\n dummy |= (arr1[i % arr1.length] || 0) ^ (arr2[i % arr2.length] || 0);\n }\n return false;\n }\n \n let result = 0;\n for (let i = 0; i < arr1.length; i++) {\n result |= arr1[i] ^ arr2[i];\n }\n \n return result === 0;\n }\n \n /**\n * CRITICAL SECURITY: Encrypt data with AAD (Additional Authenticated Data)\n * This method provides authenticated encryption with additional data binding\n */\n static async encryptDataWithAAD(data, key, aad) {\n try {\n const dataString = typeof data === 'string' ? data : JSON.stringify(data);\n const encoder = new TextEncoder();\n const dataBuffer = encoder.encode(dataString);\n const aadBuffer = encoder.encode(aad);\n\n // Generate random IV\n const iv = crypto.getRandomValues(new Uint8Array(12));\n\n // Encrypt with AAD\n const encrypted = await crypto.subtle.encrypt(\n { \n name: 'AES-GCM', \n iv: iv,\n additionalData: aadBuffer\n },\n key,\n dataBuffer\n );\n\n // Package encrypted data\n const encryptedPackage = {\n version: '1.0',\n iv: Array.from(iv),\n data: Array.from(new Uint8Array(encrypted)),\n aad: aad,\n timestamp: Date.now()\n };\n\n const packageString = JSON.stringify(encryptedPackage);\n const packageBuffer = encoder.encode(packageString);\n \n return EnhancedSecureCryptoUtils.arrayBufferToBase64(packageBuffer);\n } catch (error) {\n throw new Error(`AAD encryption failed: ${error.message}`);\n }\n }\n\n /**\n * CRITICAL SECURITY: Decrypt data with AAD validation\n * This method provides authenticated decryption with additional data validation\n */\n static async decryptDataWithAAD(encryptedData, key, expectedAad) {\n try {\n const packageBuffer = EnhancedSecureCryptoUtils.base64ToArrayBuffer(encryptedData);\n const packageString = new TextDecoder().decode(packageBuffer);\n const encryptedPackage = JSON.parse(packageString);\n\n if (!encryptedPackage.version || !encryptedPackage.iv || !encryptedPackage.data || !encryptedPackage.aad) {\n throw new Error('Invalid encrypted data format');\n }\n\n // Validate AAD matches expected\n if (encryptedPackage.aad !== expectedAad) {\n throw new Error('AAD mismatch - possible tampering or replay attack');\n }\n\n const iv = new Uint8Array(encryptedPackage.iv);\n const encrypted = new Uint8Array(encryptedPackage.data);\n const aadBuffer = new TextEncoder().encode(encryptedPackage.aad);\n\n // Decrypt with AAD validation\n const decrypted = await crypto.subtle.decrypt(\n { \n name: 'AES-GCM', \n iv: iv,\n additionalData: aadBuffer\n },\n key,\n encrypted\n );\n\n const decryptedString = new TextDecoder().decode(decrypted);\n\n try {\n return JSON.parse(decryptedString);\n } catch {\n return decryptedString;\n }\n } catch (error) {\n throw new Error(`AAD decryption failed: ${error.message}`);\n }\n }\n\n // Initialize secure logging system after class definition\n static {\n if (EnhancedSecureCryptoUtils.secureLog && typeof EnhancedSecureCryptoUtils.secureLog.init === 'function') {\n EnhancedSecureCryptoUtils.secureLog.init();\n }\n }\n}\n\nexport { EnhancedSecureCryptoUtils };\n", "// Generated by scripts/build-i18n.js from locales/*.json \u2014 do not edit by hand.\n// Add or change strings in locales/.json, then run `npm run build:i18n`.\n\nexport const DEFAULT_LOCALE = \"en\";\nexport const SUPPORTED_LOCALES = [\"en\",\"de\",\"fr\",\"es\",\"uk\",\"ru\",\"zh\",\"ko\",\"hi\",\"ar\",\"he\",\"fa\",\"ur\"];\nexport const LOCALE_META = {\n \"en\": {\n \"htmlLang\": \"en\",\n \"nativeName\": \"English\",\n \"abbr\": \"EN\",\n \"dir\": \"ltr\",\n \"path\": \"/\"\n },\n \"de\": {\n \"htmlLang\": \"de\",\n \"nativeName\": \"Deutsch\",\n \"abbr\": \"DE\",\n \"dir\": \"ltr\",\n \"path\": \"/de/\"\n },\n \"fr\": {\n \"htmlLang\": \"fr\",\n \"nativeName\": \"Fran\u00E7ais\",\n \"abbr\": \"FR\",\n \"dir\": \"ltr\",\n \"path\": \"/fr/\"\n },\n \"es\": {\n \"htmlLang\": \"es\",\n \"nativeName\": \"Espa\u00F1ol\",\n \"abbr\": \"ES\",\n \"dir\": \"ltr\",\n \"path\": \"/es/\"\n },\n \"uk\": {\n \"htmlLang\": \"uk\",\n \"nativeName\": \"\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430\",\n \"abbr\": \"UK\",\n \"dir\": \"ltr\",\n \"path\": \"/uk/\"\n },\n \"ru\": {\n \"htmlLang\": \"ru\",\n \"nativeName\": \"\u0420\u0443\u0441\u0441\u043A\u0438\u0439\",\n \"abbr\": \"RU\",\n \"dir\": \"ltr\",\n \"path\": \"/ru/\"\n },\n \"zh\": {\n \"htmlLang\": \"zh-Hans\",\n \"nativeName\": \"\u7B80\u4F53\u4E2D\u6587\",\n \"abbr\": \"ZH\",\n \"dir\": \"ltr\",\n \"path\": \"/zh/\"\n },\n \"ko\": {\n \"htmlLang\": \"ko\",\n \"nativeName\": \"\uD55C\uAD6D\uC5B4\",\n \"abbr\": \"KO\",\n \"dir\": \"ltr\",\n \"path\": \"/ko/\"\n },\n \"hi\": {\n \"htmlLang\": \"hi\",\n \"nativeName\": \"\u0939\u093F\u0928\u094D\u0926\u0940\",\n \"abbr\": \"HI\",\n \"dir\": \"ltr\",\n \"path\": \"/hi/\"\n },\n \"ar\": {\n \"htmlLang\": \"ar\",\n \"nativeName\": \"\u0627\u0644\u0639\u0631\u0628\u064A\u0629\",\n \"abbr\": \"AR\",\n \"dir\": \"rtl\",\n \"path\": \"/ar/\"\n },\n \"he\": {\n \"htmlLang\": \"he\",\n \"nativeName\": \"\u05E2\u05D1\u05E8\u05D9\u05EA\",\n \"abbr\": \"HE\",\n \"dir\": \"rtl\",\n \"path\": \"/he/\"\n },\n \"fa\": {\n \"htmlLang\": \"fa\",\n \"nativeName\": \"\u0641\u0627\u0631\u0633\u06CC\",\n \"abbr\": \"FA\",\n \"dir\": \"rtl\",\n \"path\": \"/fa/\"\n },\n \"ur\": {\n \"htmlLang\": \"ur\",\n \"nativeName\": \"\u0627\u0631\u062F\u0648\",\n \"abbr\": \"UR\",\n \"dir\": \"rtl\",\n \"path\": \"/ur/\"\n }\n};\n\n// Strings a page may need for a locale it did not load. See CROSS_LOCALE_KEYS in\n// scripts/build-i18n.js for why this list is short on purpose.\nexport const CROSS_LOCALE_STRINGS = {\n \"en\": {\n \"language.suggest.text\": \"This page is also available in English.\",\n \"language.suggest.cta\": \"Read in English\",\n \"language.suggest.dismiss\": \"Dismiss\"\n },\n \"de\": {\n \"language.suggest.text\": \"Diese Seite gibt es auch auf Deutsch.\",\n \"language.suggest.cta\": \"Auf Deutsch lesen\",\n \"language.suggest.dismiss\": \"Schlie\u00DFen\"\n },\n \"fr\": {\n \"language.suggest.text\": \"Cette page est aussi disponible en fran\u00E7ais.\",\n \"language.suggest.cta\": \"Lire en fran\u00E7ais\",\n \"language.suggest.dismiss\": \"Fermer\"\n },\n \"es\": {\n \"language.suggest.text\": \"Esta p\u00E1gina tambi\u00E9n est\u00E1 disponible en espa\u00F1ol.\",\n \"language.suggest.cta\": \"Leer en espa\u00F1ol\",\n \"language.suggest.dismiss\": \"Cerrar\"\n },\n \"uk\": {\n \"language.suggest.text\": \"\u0426\u044F \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0430 \u0442\u0430\u043A\u043E\u0436 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0443\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u043E\u044E.\",\n \"language.suggest.cta\": \"\u0427\u0438\u0442\u0430\u0442\u0438 \u0443\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u043E\u044E\",\n \"language.suggest.dismiss\": \"\u0417\u0430\u043A\u0440\u0438\u0442\u0438\"\n },\n \"ru\": {\n \"language.suggest.text\": \"\u042D\u0442\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0442\u0430\u043A\u0436\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u0430 \u0440\u0443\u0441\u0441\u043A\u043E\u043C.\",\n \"language.suggest.cta\": \"\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E-\u0440\u0443\u0441\u0441\u043A\u0438\",\n \"language.suggest.dismiss\": \"\u0417\u0430\u043A\u0440\u044B\u0442\u044C\"\n },\n \"zh\": {\n \"language.suggest.text\": \"\u672C\u9875\u9762\u4E5F\u6709\u7B80\u4F53\u4E2D\u6587\u7248\u672C\u3002\",\n \"language.suggest.cta\": \"\u9605\u8BFB\u7B80\u4F53\u4E2D\u6587\",\n \"language.suggest.dismiss\": \"\u5173\u95ED\"\n },\n \"ko\": {\n \"language.suggest.text\": \"\uC774 \uD398\uC774\uC9C0\uB294 \uD55C\uAD6D\uC5B4\uB85C\uB3C4 \uBCFC \uC218 \uC788\uC2B5\uB2C8\uB2E4.\",\n \"language.suggest.cta\": \"\uD55C\uAD6D\uC5B4\uB85C \uBCF4\uAE30\",\n \"language.suggest.dismiss\": \"\uB2EB\uAE30\"\n },\n \"hi\": {\n \"language.suggest.text\": \"\u092F\u0939 \u092A\u0943\u0937\u094D\u0920 \u0939\u093F\u0928\u094D\u0926\u0940 \u092E\u0947\u0902 \u092D\u0940 \u0909\u092A\u0932\u092C\u094D\u0927 \u0939\u0948\u0964\",\n \"language.suggest.cta\": \"\u0939\u093F\u0928\u094D\u0926\u0940 \u092E\u0947\u0902 \u092A\u0922\u093C\u0947\u0902\",\n \"language.suggest.dismiss\": \"\u092C\u0902\u0926 \u0915\u0930\u0947\u0902\"\n },\n \"ar\": {\n \"language.suggest.text\": \"\u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629 \u0645\u062A\u0648\u0641\u0631\u0629 \u0623\u064A\u0636\u064B\u0627 \u0628\u0627\u0644\u0639\u0631\u0628\u064A\u0629.\",\n \"language.suggest.cta\": \"\u0627\u0642\u0631\u0623 \u0628\u0627\u0644\u0639\u0631\u0628\u064A\u0629\",\n \"language.suggest.dismiss\": \"\u0625\u063A\u0644\u0627\u0642\"\n },\n \"he\": {\n \"language.suggest.text\": \"\u05D4\u05D3\u05E3 \u05D4\u05D6\u05D4 \u05D6\u05DE\u05D9\u05DF \u05D2\u05DD \u05D1\u05E2\u05D1\u05E8\u05D9\u05EA.\",\n \"language.suggest.cta\": \"\u05DC\u05E7\u05E8\u05D9\u05D0\u05D4 \u05D1\u05E2\u05D1\u05E8\u05D9\u05EA\",\n \"language.suggest.dismiss\": \"\u05E1\u05D2\u05D9\u05E8\u05D4\"\n },\n \"fa\": {\n \"language.suggest.text\": \"\u0627\u06CC\u0646 \u0635\u0641\u062D\u0647 \u0628\u0647 \u0641\u0627\u0631\u0633\u06CC \u0647\u0645 \u062F\u0631 \u062F\u0633\u062A\u0631\u0633 \u0627\u0633\u062A.\",\n \"language.suggest.cta\": \"\u062E\u0648\u0627\u0646\u062F\u0646 \u0628\u0647 \u0641\u0627\u0631\u0633\u06CC\",\n \"language.suggest.dismiss\": \"\u0628\u0633\u062A\u0646\"\n },\n \"ur\": {\n \"language.suggest.text\": \"\u06CC\u06C1 \u0635\u0641\u062D\u06C1 \u0627\u0631\u062F\u0648 \u0645\u06CC\u06BA \u0628\u06BE\u06CC \u062F\u0633\u062A\u06CC\u0627\u0628 \u06C1\u06D2\u06D4\",\n \"language.suggest.cta\": \"\u0627\u0631\u062F\u0648 \u0645\u06CC\u06BA \u067E\u0691\u06BE\u06CC\u06BA\",\n \"language.suggest.dismiss\": \"\u0628\u0646\u062F \u06A9\u0631\u06CC\u06BA\"\n }\n};\n", "// Generated by scripts/build-i18n.js from locales/*.json \u2014 do not edit by hand.\n// Add or change strings in locales/.json, then run `npm run build:i18n`.\n\nexport const DICTIONARY = {\n \"language.label\": \"Language\",\n \"language.suggest.text\": \"This page is also available in English.\",\n \"language.suggest.cta\": \"Read in English\",\n \"language.suggest.dismiss\": \"Dismiss\",\n \"theme.label\": \"Theme\",\n \"theme.system\": \"System\",\n \"theme.light\": \"Light\",\n \"theme.dark\": \"Dark\",\n \"community.title\": \"Join the future of privacy\",\n \"community.description\": \"SecureBit grows thanks to its community. Your ideas and feedback shape the future of secure communication - built in the open, with complete ASN.1 validation end-to-end.\",\n \"community.github\": \"GitHub Repository\",\n \"community.feedback\": \"Feedback\",\n \"unique.eyebrow\": \"What sets us apart\",\n \"unique.heading\": \"Why SecureBit is unique\",\n \"unique.s1.titleTop\": \"Layered\",\n \"unique.s1.titleBottom\": \"encryption core\",\n \"unique.s1.collapsed\": \"Encryption core\",\n \"unique.s1.desc\": \"ECDH P-384 key exchange, AES-256-GCM payloads, ECDSA signatures and full ASN.1 validation - composed into one hardened pipeline.\",\n \"unique.s1.tags\": [\n \"ECDH P-384\",\n \"AES-256-GCM\",\n \"ECDSA\",\n \"ASN.1\"\n ],\n \"unique.s2.titleTop\": \"Pure P2P\",\n \"unique.s2.titleBottom\": \"WebRTC\",\n \"unique.s2.collapsed\": \"Pure P2P WebRTC\",\n \"unique.s2.desc\": \"Messages travel directly between devices over WebRTC. No relay holds your data - the server only helps two peers find each other.\",\n \"unique.s2.tags\": [\n \"DTLS 1.3\",\n \"No relay\"\n ],\n \"unique.s3.titleTop\": \"Perfect\",\n \"unique.s3.titleBottom\": \"forward secrecy\",\n \"unique.s3.collapsed\": \"Forward secrecy\",\n \"unique.s3.desc\": \"Session keys rotate continuously and are discarded after use, so a single compromised key can never unlock past conversations.\",\n \"unique.s3.tags\": [\n \"Ephemeral keys\",\n \"Auto-rotate\"\n ],\n \"unique.s4.titleTop\": \"Traffic\",\n \"unique.s4.titleBottom\": \"obfuscation\",\n \"unique.s4.collapsed\": \"Traffic obfuscation\",\n \"unique.s4.desc\": \"Packet sizes and timing are padded and randomized, hiding metadata patterns from anyone watching the wire.\",\n \"unique.s4.tags\": [\n \"Packet padding\",\n \"Timing jitter\"\n ],\n \"unique.s5.titleTop\": \"Zero data\",\n \"unique.s5.titleBottom\": \"collection\",\n \"unique.s5.collapsed\": \"Zero data collection\",\n \"unique.s5.desc\": \"No accounts, no logs, no message storage. There is nothing on a server to leak, subpoena, or sell.\",\n \"unique.s5.tags\": [\n \"No accounts\",\n \"No logs\"\n ],\n \"partners.eyebrow\": \"Partners & ecosystem\",\n \"partners.heading\": \"Trusted by our partners\",\n \"partners.aegis.desc\": \"Capital partner securing confidential financial communications across its portfolio.\",\n \"partners.aegis.role\": \"Strategic backer\",\n \"partners.furilabs.desc\": \"Privacy-first Linux phones that ship SecureBit as a default secure channel.\",\n \"partners.furilabs.role\": \"Technology partner\",\n \"partners.inviteTitle\": \"Become a partner\",\n \"partners.inviteDesc\": \"Building privacy hardware or infrastructure? Let's integrate SecureBit.\",\n \"partners.inviteCta\": \"Start a conversation\",\n \"roadmap.eyebrow\": \"Development Roadmap\",\n \"roadmap.heading\": \"The evolution of SecureBit\",\n \"roadmap.subheading\": \"From the first prototype to a quantum-resistant, decentralized network - with complete ASN.1 validation at every layer.\",\n \"roadmap.progress\": \"{shipped} of {total} milestones shipped\",\n \"roadmap.upcoming\": \"{upcoming} on the way\",\n \"roadmap.keyFeatures\": \"Key features\",\n \"roadmap.status.released\": \"Released\",\n \"roadmap.status.current\": \"Current\",\n \"roadmap.status.dev\": \"In development\",\n \"roadmap.status.planned\": \"Planned\",\n \"roadmap.status.research\": \"Research\",\n \"hero.headlineTop\": \"A direct line\",\n \"hero.headlineBottom\": \"only you two can read.\",\n \"hero.subheading\": \"Keys are generated on your device and exchanged peer-to-peer. No accounts, no servers storing your messages.\",\n \"hero.diagramAlt\": \"A direct encrypted line to one peer, with two more peers joining the mesh\",\n \"intro.createTitle\": \"Create a new channel\",\n \"intro.createDesc\": \"Your device generates the keys and a one-time invitation. Nothing touches a server.\",\n \"intro.createCta\": \"Generate keys & invitation\",\n \"intro.joinTitle\": \"Join a channel\",\n \"intro.scanTitle\": \"Scan QR with camera\",\n \"intro.scanSubtitle\": \"Fastest - point at your peer's screen\",\n \"intro.orPasteCode\": \"or paste code\",\n \"intro.pastePlaceholder\": \"Paste invitation code here\u2026\",\n \"intro.connect\": \"Connect\",\n \"intro.connecting\": \"Processing\u2026\",\n \"roadmap.r1.title\": \"Start of Development\",\n \"roadmap.r1.sub\": \"Idea, prototype, and infrastructure setup\",\n \"roadmap.r1.date\": \"Early 2025\",\n \"roadmap.r1.features\": [\n \"Concept and requirements formation\",\n \"Stack selection: WebRTC, P2P, cryptography\",\n \"First messaging prototypes\",\n \"Repository creation and CI\",\n \"Basic encryption architecture\",\n \"UX/UI design\"\n ],\n \"roadmap.r2.title\": \"Alpha Release\",\n \"roadmap.r2.sub\": \"First public alpha: basic chat and key exchange\",\n \"roadmap.r2.date\": \"Spring 2025\",\n \"roadmap.r2.features\": [\n \"Basic P2P messaging via WebRTC\",\n \"Simple E2E encryption (demo scheme)\",\n \"Stable signaling and reconnection\",\n \"Minimal UX for testing\",\n \"Feedback collection from early testers\"\n ],\n \"roadmap.r3.title\": \"Security Hardened\",\n \"roadmap.r3.sub\": \"Security strengthening and stable branch release\",\n \"roadmap.r3.date\": \"Summer 2025\",\n \"roadmap.r3.features\": [\n \"ECDH/ECDSA implementation in production\",\n \"Perfect Forward Secrecy and key rotation\",\n \"Improved authentication checks\",\n \"File encryption and large payload transfers\",\n \"Audit of basic cryptoprocesses\"\n ],\n \"roadmap.r4.title\": \"Scaling & Stability\",\n \"roadmap.r4.sub\": \"Network scaling and stability improvements\",\n \"roadmap.r4.date\": \"Fall 2025\",\n \"roadmap.r4.features\": [\n \"Optimization of P2P connections and NAT traversal\",\n \"Reconnection mechanisms and message queues\",\n \"Reduced battery consumption on mobile\",\n \"Multi-device synchronization support\",\n \"Monitoring and logging tools for developers\"\n ],\n \"roadmap.r5.title\": \"Privacy-first Release\",\n \"roadmap.r5.sub\": \"Focus on privacy: minimizing metadata\",\n \"roadmap.r5.date\": \"Winter 2025\",\n \"roadmap.r5.features\": [\n \"Metadata protection and fingerprint reduction\",\n \"Experiments with onion routing and DHT\",\n \"Options for anonymous connections\",\n \"Preparation for open code audit\",\n \"Improved user verification processes\"\n ],\n \"roadmap.r6.title\": \"Enhanced Security Edition\",\n \"roadmap.r6.sub\": \"18-layer military-grade cryptography with complete ASN.1 validation\",\n \"roadmap.r6.date\": \"Late 2025\",\n \"roadmap.r6.features\": [\n \"ECDH + DTLS + SAS triple-layer security\",\n \"ECDH P-384 + AES-GCM 256-bit encryption\",\n \"DTLS fingerprint verification\",\n \"SAS (Short Authentication String) verification\",\n \"Perfect Forward Secrecy with key rotation\",\n \"Enhanced MITM attack prevention\",\n \"Complete ASN.1 DER validation\",\n \"OID and EC point verification\",\n \"SPKI structure validation\",\n \"P2P WebRTC architecture\",\n \"Metadata protection\",\n \"100% open source code\"\n ],\n \"roadmap.r7.title\": \"Desktop Edition\",\n \"roadmap.r7.sub\": \"Native desktop apps for Windows, macOS, and Linux\",\n \"roadmap.r7.date\": \"Early 2026\",\n \"roadmap.r7.features\": [\n \"Windows desktop app (Tauri v2)\",\n \"macOS desktop app (Tauri v2)\",\n \"Linux AppImage support (Tauri v2)\",\n \"Real-time notifications\",\n \"Automatic reconnection\",\n \"Cross-device synchronization\",\n \"Improved UX/UI\",\n \"Support for files up to 100MB\"\n ],\n \"roadmap.r8.title\": \"Secure Voice & Calls\",\n \"roadmap.r8.sub\": \"Encrypted voice messages, audio calls, and video calls\",\n \"roadmap.r8.date\": \"Early 2026\",\n \"roadmap.r8.features\": [\n \"End-to-end encrypted voice messages\",\n \"1:1 encrypted audio calls (WebRTC)\",\n \"1:1 encrypted video calls (WebRTC)\",\n \"Perfect Forward Secrecy for live media\",\n \"SRTP/DTLS-protected media streams\",\n \"In-call SAS verification\",\n \"Call notifications and auto-reconnection\",\n \"Low-latency P2P media\"\n ],\n \"roadmap.r9.title\": \"Group Communications\",\n \"roadmap.r9.sub\": \"Group chats with preserved privacy\",\n \"roadmap.r9.date\": \"Now\",\n \"roadmap.r9.features\": [\n \"P2P group chats up to 8 participants\",\n \"Mesh delivery with signed relay fallback\",\n \"One group safety code, compared by everyone\",\n \"Commit-then-reveal ceremony against code grinding\",\n \"Per-group identity keys, ephemeral by design\",\n \"Signed membership with epoch ordering\",\n \"Signed messages, so a split transcript is provable\",\n \"No server, no shared group key, no history\"\n ],\n \"roadmap.r10.title\": \"Mobile Edition\",\n \"roadmap.r10.sub\": \"Native mobile apps for iOS and Android\",\n \"roadmap.r10.date\": \"Q2 2027\",\n \"roadmap.r10.features\": [\n \"iOS native app (Swift/SwiftUI)\",\n \"Android native app (Kotlin/Jetpack Compose)\",\n \"PWA support for mobile browsers\",\n \"Real-time push notifications\",\n \"Battery optimization\",\n \"Mobile-optimized UX/UI\",\n \"Offline message queuing\",\n \"Biometric authentication\"\n ],\n \"roadmap.r11.title\": \"Quantum-Resistant Edition\",\n \"roadmap.r11.sub\": \"Protection against quantum computers\",\n \"roadmap.r11.date\": \"Q4 2027\",\n \"roadmap.r11.features\": [\n \"Post-quantum cryptography CRYSTALS-Kyber\",\n \"SPHINCS+ digital signatures\",\n \"Hybrid scheme: classic + PQ\",\n \"Quantum-safe key exchange\",\n \"Updated hashing algorithms\",\n \"Migration of existing sessions\",\n \"Compatibility with v5.x\",\n \"Quantum-resistant protocols\"\n ],\n \"roadmap.r12.title\": \"Decentralized Network\",\n \"roadmap.r12.sub\": \"Fully decentralized network\",\n \"roadmap.r12.date\": \"2028\",\n \"roadmap.r12.features\": [\n \"Node mesh network\",\n \"DHT for peer discovery\",\n \"Built-in onion routing\",\n \"Tokenomics and node incentives\",\n \"Governance via DAO\",\n \"Interoperability with other networks\",\n \"Cross-platform compatibility\",\n \"Self-healing network\"\n ],\n \"roadmap.r13.title\": \"AI Privacy Assistant\",\n \"roadmap.r13.sub\": \"AI for privacy and security\",\n \"roadmap.r13.date\": \"2028+\",\n \"roadmap.r13.features\": [\n \"Local AI threat analysis\",\n \"Automatic MITM detection\",\n \"Adaptive cryptography\",\n \"Personalized security recommendations\",\n \"Zero-knowledge machine learning\",\n \"Private AI assistant\",\n \"Predictive security\",\n \"Autonomous attack protection\"\n ],\n \"handshake.step1\": \"Generating ECDH P-384 key pair\",\n \"handshake.step2\": \"Deriving verification code\",\n \"handshake.step3\": \"Pinning Perfect Forward Secrecy\",\n \"handshake.securingTitle\": \"Securing your channel\",\n \"handshake.answerTitle\": \"Building your answer\",\n \"handshake.securingDesc\": \"Forging keys strong enough to resist tampering.\",\n \"handshake.shareTitle\": \"Share your invitation\",\n \"handshake.sendAnswerTitle\": \"Send back your answer\",\n \"handshake.shareDesc\": \"Show the QR or send the code to your peer. It is one-time and expires shortly.\",\n \"handshake.sendAnswerDesc\": \"Give this answer to the channel creator so they can finish the handshake.\",\n \"handshake.establish\": \"Establish connection\",\n \"handshake.thenReceive\": \"Then receive the answer your peer sends back\",\n \"handshake.pasteAnswerPlaceholder\": \"Paste peer's answer code\u2026\",\n \"handshake.answerSentNote\": \"Send this answer to the creator, then wait - the chat opens once they connect.\",\n \"handshake.qrHint\": \"Keep this open until your peer captures the code.\",\n \"handshake.qrHintFrames\": \"The handshake is split across {frames} frames - keep this open until your peer captures all of them.\",\n \"verify.title\": \"Security verification\",\n \"verify.desc\": \"Compare this safety code with your peer over a separate channel (voice / in person), then type it to unlock the chat.\",\n \"verify.enterLabel\": \"Enter the verified code\",\n \"verify.placeholder\": \"Type code here\",\n \"verify.confirm\": \"Confirm code\",\n \"verify.confirmed\": \"Confirmed\",\n \"verify.mismatch\": \"Don't match\",\n \"verify.yours\": \"Your confirmation\",\n \"verify.peer\": \"Peer confirmation\",\n \"verify.pending\": \"Pending\",\n \"verify.waiting\": \"Waiting for code\u2026\",\n \"verify.verified\": \"Channel verified\",\n \"verify.bothConfirmed\": \"Both parties confirmed. Opening the secure chat\u2026\",\n \"pwa.bannerTitle\": \"Install SecureBit.chat\",\n \"pwa.bannerDesc\": \"Get the native app experience with enhanced security\",\n \"pwa.install\": \"Install\",\n \"pwa.dismiss\": \"Dismiss\",\n \"pwa.close\": \"Close\",\n \"pwa.iosTitle\": \"Install on iOS\",\n \"pwa.iosStep1\": \"Tap the Share button\",\n \"pwa.iosStep1Hint\": \"Usually at the bottom of Safari\",\n \"pwa.iosStep2\": \"Find \\\"Add to Home Screen\\\"\",\n \"pwa.iosStep2Hint\": \"Scroll down in the share menu\",\n \"pwa.iosStep3\": \"Tap \\\"Add\\\"\",\n \"pwa.iosStep3Hint\": \"Confirm to install SecureBit.chat\",\n \"pwa.genericTitle\": \"Install SecureBit\",\n \"pwa.genericDesc\": \"Your browser handles installs its own way. Pick the steps that match yours.\",\n \"pwa.androidChromeHint\": \"Tap \u22EE in the top corner, then \u201CInstall app\u201D\",\n \"pwa.androidOtherHint\": \"Tap the menu, then \u201CAdd to Home screen\u201D\",\n \"pwa.gotIt\": \"Got it\",\n \"pwa.installedTitle\": \"App Installed!\",\n \"pwa.installedIos\": \"iOS App installed! Open from home screen.\",\n \"pwa.installedGeneric\": \"SecureBit.chat is now on your device\",\n \"pwa.anytimeTitle\": \"Install Anytime\",\n \"pwa.anytimeDesc\": \"You can still install SecureBit.chat from your browser's menu for the best experience.\",\n \"pwa.ok\": \"OK\",\n \"step.open\": \"Step 1 \u00B7 open a channel\",\n \"step.exchange\": \"Step 2 \u00B7 exchange\",\n \"step.verification\": \"Step 3 \u00B7 verification\",\n \"action.create\": \"Create\",\n \"action.join\": \"Join\",\n \"action.back\": \"Back\",\n \"action.copy\": \"Copy\",\n \"action.copied\": \"Copied\",\n \"action.scan\": \"Scan\",\n \"action.downloadDesktop\": \"Download desktop app\",\n \"action.advancedSettings\": \"Advanced settings\",\n \"cred.offerTag\": \"offer \u00B7 or copy text\",\n \"cred.answerTag\": \"answer \u00B7 or copy text\",\n \"cred.reveal\": \"Click to reveal - keep this code private\",\n \"qr.title\": \"Scan QR code\",\n \"qr.subtitle\": \"Point your camera at their QR\",\n \"qr.scanning\": \"Scanning\u2026\",\n \"qr.hint\": \"Hold steady until all parts are captured. Camera access is local - nothing is uploaded.\",\n \"mesh.you\": \"you\",\n \"mesh.peer\": \"peer \u00B7 session 1\",\n \"mesh.joined2\": \"mara joined \u00B7 +2\",\n \"mesh.joined3\": \"tobi joined \u00B7 +3\",\n \"ice.title\": \"Network settings\",\n \"ice.subtitle\": \"Configured locally - never shared with your peer\",\n \"ice.intro\": \"SecureBit uses public STUN servers by default to negotiate the peer-to-peer link. Point it at your own STUN/TURN if you self-host.\",\n \"ice.publicTitle\": \"Public servers (default)\",\n \"ice.publicDesc\": \"Zero-config. Good for most users.\",\n \"ice.customTitle\": \"My own STUN/TURN servers\",\n \"ice.customDesc\": \"Up to {max} servers.\",\n \"ice.turnNote\": \"A TURN relay sees both peers' IP and traffic timing - but never message contents, which stay end-to-end encrypted. Prefer \",\n \"ice.turnNoteTls\": \" (TLS).\",\n \"ice.test\": \"Test servers\",\n \"ice.testing\": \"Testing\u2026\",\n \"ice.relayTitle\": \"Relay-only mode\",\n \"ice.relayBadge\": \"MAX PRIVACY\",\n \"ice.relayDesc\": \"Routes all traffic through TURN so your IP is never exposed to the peer. Requires a TURN server.\",\n \"ice.relayWarning\": \"Relay-only is enabled but no TURN server is configured. The connection will not be able to start.\",\n \"ice.persist\": \"Save on this device\",\n \"ice.persistDesc\": \"Stored encrypted in this browser. Leave off to use only for this session.\",\n \"ice.forget\": \"Forget saved\",\n \"ice.cancel\": \"Cancel\",\n \"ice.apply\": \"Apply\",\n \"ice.errUnavailable\": \"WebRTC is not available in this browser\",\n \"ice.errInvalid\": \"Invalid server configuration\",\n \"update.title\": \"Update available\",\n \"update.desc\": \"A newer version of SecureBit has been detected.\",\n \"update.currentVersion\": \"Current version\",\n \"update.newVersion\": \"New version\",\n \"update.now\": \"Update now\",\n \"update.later\": \"Later\",\n \"update.unknown\": \"N/A\",\n \"update.stepSaving\": \"Saving data...\",\n \"update.stepSwCaches\": \"Clearing Service Worker caches...\",\n \"update.stepSwUnregister\": \"Unregistering Service Workers...\",\n \"update.stepBrowserCache\": \"Clearing browser cache...\",\n \"update.stepVersion\": \"Updating version...\",\n \"update.stepReload\": \"Reloading application...\",\n \"update.error\": \"Update error. Please refresh the page manually (Ctrl+F5 or Cmd+Shift+R)\",\n \"update.confirmSkip\": \"New version available. Update is recommended for security and stability. Continue without update?\",\n \"offline.backOnline\": \"Back online\",\n \"offline.mode\": \"Offline mode\",\n \"offline.dismiss\": \"Dismiss\",\n \"offline.lostTitle\": \"Connection lost\",\n \"offline.lostDesc\": \"SecureBit is now in offline mode. Some features are limited, but your data stays safe.\",\n \"offline.point1\": \"Your session and keys are preserved\",\n \"offline.point2\": \"No data is stored on servers\",\n \"offline.point3\": \"Messages & files sync when you reconnect\",\n \"offline.continue\": \"Continue offline\",\n \"offline.restoredTitle\": \"When you reconnect\",\n \"offline.restoredDesc\": \"A dropped connection costs you nothing. SecureBit queues everything locally and resumes the encrypted session the instant you're back online.\",\n \"offline.r1Title\": \"Your messages get delivered\",\n \"offline.r1Desc\": \"Everything you wrote while offline is sent to your contact automatically.\",\n \"offline.r2Title\": \"Files finish transferring\",\n \"offline.r2Desc\": \"Uploads resume from where they stopped - no need to resend.\",\n \"offline.r3Title\": \"Their messages & files arrive\",\n \"offline.r3Desc\": \"Whatever your contact sent during the outage is delivered to you in order.\",\n \"offline.r4Title\": \"Nothing is lost\",\n \"offline.r4Desc\": \"After reconnect there's no gap - the conversation continues exactly where it paused.\",\n \"offline.gotIt\": \"Got it\",\n \"qr.showTitle\": \"Show QR code\",\n \"qr.showSubtitle\": \"Full-screen \u00B7 let your peer scan\",\n \"qr.showSubtitleFrames\": \"Full-screen \u00B7 let your peer scan all {frames} frames\",\n \"notify.enabledBody\": \"Notifications enabled! You will receive alerts for new messages.\",\n \"notify.workingBody\": \"Notifications are working! You will receive alerts for new messages.\",\n \"pw.label\": \"Password input\",\n \"pw.placeholder\": \"Enter password...\",\n \"pw.decrypt\": \"Decrypt\",\n \"pw.cancel\": \"Cancel\",\n \"file.title\": \"File transfers\",\n \"file.drop\": \"Drag & drop files here\",\n \"file.dropHint\": \"Encrypted end-to-end before transfer \u00B7 up to 100 MB\",\n \"file.browse\": \"Browse device\",\n \"file.incoming\": \"Incoming file request\",\n \"file.accept\": \"Accept\",\n \"file.reject\": \"Reject\",\n \"file.download\": \"Download\",\n \"file.notReady\": \"Connection not ready\",\n \"file.tooLarge\": \"File too large\",\n \"file.typeNotAllowed\": \"File type not allowed\",\n \"file.maxConcurrent\": \"Maximum concurrent transfers\",\n \"file.gone\": \"This file is no longer available for download.\",\n \"call.incoming\": \"Incoming call\",\n \"call.incomingVideo\": \"Incoming video call\",\n \"call.encrypted\": \"Encrypted call\",\n \"call.encryptedShort\": \"Encrypted\",\n \"call.peer\": \"Secure peer\",\n \"call.connecting\": \"Connecting\u2026\",\n \"call.ringing\": \"Ringing\u2026\",\n \"call.accept\": \"Accept\",\n \"call.decline\": \"Decline\",\n \"call.end\": \"End call\",\n \"call.camera\": \"Camera\",\n \"call.cameraOff\": \"Camera off\",\n \"call.peerCameraOff\": \"Peer's camera is off\",\n \"call.flipCamera\": \"Flip camera\",\n \"call.addVideo\": \"Add video\",\n \"call.video\": \"Video\",\n \"call.muted\": \"Muted\",\n \"call.expand\": \"Expand\",\n \"call.minimize\": \"Minimize\",\n \"call.quality\": \"Connection quality\",\n \"call.qualityExcellent\": \"Excellent\",\n \"call.videoPrefix\": \"Video \u00B7 \",\n \"call.voicePrefix\": \"Voice \u00B7 \",\n \"group.new\": \"New group\",\n \"group.create\": \"Create group\",\n \"group.join\": \"Join group\",\n \"group.name\": \"Group name\",\n \"group.nameTooLong\": \"The group name is too long.\",\n \"group.capacity\": \"Up to {max} people, peer to peer. Everyone will compare one safety code before the group opens.\",\n \"group.invite\": \"Invite\",\n \"group.inviteMore\": \"Invite more members\",\n \"group.invitePeople\": \"Invite {count} people\",\n \"group.invitation\": \"Group invitation\",\n \"group.addMembers\": \"Add members\",\n \"group.roomFor\": \"Room for {remaining} more. Everyone will compare a new group code once they join.\",\n \"group.membersCount\": \"{count} members\",\n \"group.member\": \"Member\",\n \"group.members\": \"Members\",\n \"group.remove\": \"Remove {name}\",\n \"group.message\": \"Message {name}\",\n \"group.leave\": \"Leave\",\n \"group.leaveThis\": \"Leave this group\",\n \"group.cancel\": \"Cancel group\",\n \"group.close\": \"Close\",\n \"group.cancelBtn\": \"Cancel\",\n \"group.decline\": \"Decline\",\n \"group.working\": \"Working\u2026\",\n \"group.sasTitle\": \"Group safety code\",\n \"group.sasEveryone\": \"Everyone sees this code\",\n \"group.sasCompare\": \"Compare the group code with every member to open this group.\",\n \"group.sasReadAloud\": \"Read these digits aloud to \",\n \"group.sasWarning\": \"If even one member reads a different code, someone is sitting between you. Cancel the group - do not confirm.\",\n \"group.confirmFirst\": \"Confirm the group code first\",\n \"group.codeSuffix\": \" \u00B7 code {code}\",\n \"group.exchangingNonces\": \"Exchanging nonces\u2026\",\n \"group.waitingCommit\": \"Waiting for every member to commit\u2026\",\n \"group.waitingJoin\": \"Waiting for the other members to join\u2026\",\n \"group.emptyChat\": \"Nothing here yet. Messages are signed by their sender and travel over each member's own encrypted link.\",\n \"group.noVerified\": \"No verified chats yet. Open a 1:1 chat and compare its safety code first - a group is built out of connections you have already checked.\",\n \"group.noMoreToAdd\": \"No other verified chats to add. Open a 1:1 chat and compare its safety code first.\",\n \"group.directLink\": \"Direct peer-to-peer link\",\n \"group.noDirectLink\": \"No direct link yet - messages are relayed by another member while one is being built\",\n \"group.relayNote\": \"Some members have no direct link to you yet. Their messages travel through another member, who can see that you are talking but cannot read past the signature or change what you said. The group keeps trying to connect them directly.\",\n \"group.relayOnlyOff\": \"Relay-only mode is off, so each member connects to you directly and learns your IP address - including members somebody else invited. Turn it on in network settings if that matters here.\",\n \"group.memberOffline\": \"{name} is offline and will not receive messages. They are still a member - removing them re-keys the group.\",\n \"group.joinNote\": \"Other members will learn your presence in this group. There is no message history to catch up on - a group starts empty.\",\n \"group.inviteSentNote\": \"The group keeps working until they accept. There is no history for them to catch up on - they will only see what is sent from now on.\",\n \"group.errNotCreated\": \"Group not created\",\n \"group.errNobodyAccepted\": \"Nobody accepted the invitation in time.\",\n \"group.errNothingSent\": \"Nothing was sent and nothing was verified. Close this and try again once everyone is connected.\",\n \"group.errNotFormed\": \"This group could not be formed.\",\n \"group.errFull\": \"This group is full.\",\n \"group.errInviteFailed\": \"The invitation could not be sent - that chat is not connected.\",\n \"group.errNoMemberList\": \"The group owner never sent the member list.\",\n \"group.errUnsignedList\": \"The member list was not signed by the group owner. Do not retry - tell them.\",\n \"group.errNotOwner\": \"Someone other than the group owner tried to change the members.\",\n \"hdr.tagline\": \"End-to-end encrypted\",\n \"hdr.netSettings\": \"Advanced network settings\",\n \"hdr.netSettingsTitle\": \"Advanced network settings (STUN/TURN)\",\n \"hdr.disconnect\": \"Disconnect\",\n \"status.connected\": \"Connected\",\n \"status.connecting\": \"Connecting...\",\n \"status.notConnected\": \"Not connected\",\n \"status.reconnecting\": \"Reconnecting...\",\n \"status.retrying\": \"Retrying...\",\n \"status.verifying\": \"Verifying...\",\n \"status.peerDisconnected\": \"Peer disconnected\",\n \"sec.replayProtection\": \"Replay Protection\",\n \"sec.messageIntegrity\": \"Message Integrity (HMAC)\",\n \"sec.forwardSecrecy\": \"Perfect Forward Secrecy\",\n \"sec.metadataProtection\": \"Metadata Protection\",\n \"sec.trafficObfuscation\": \"Traffic Obfuscation\",\n \"sec.realTests\": \"Real Cryptographic Tests\",\n \"sec.simulatedData\": \"Simulated Data\",\n \"sec.testPassed\": \"Test passed\",\n \"sec.testFailed\": \"Test failed or unavailable\",\n \"sec.verificationDone\": \"Real cryptographic verification completed\",\n \"sec.verificationInProgress\": \"Security verification in progress...\",\n \"sec.verificationWait\": \"Security verification in progress...\\nPlease wait for real-time cryptographic verification to complete.\",\n \"sec.verificationUnavailable\": \"Security verification not available\",\n \"chat.placeholder\": \"Type an encrypted message\u2026\",\n \"chat.send\": \"Send message\",\n \"chat.sendFiles\": \"Send files\",\n \"chat.hideFiles\": \"Hide files\",\n \"chat.recordVoice\": \"Record voice message\",\n \"chat.sendVoice\": \"Send voice message\",\n \"chat.discard\": \"Discard\",\n \"chat.codeBlock\": \"Send as a code block (expands the input)\",\n \"chat.codeHint\": \"Code snippet \u00B7 formatting preserved \u00B7 \u2318\u21B5 to send\",\n \"chat.viewOnce\": \"View once\",\n \"chat.viewOnceTitle\": \"View once - vanishes after the peer reads it\",\n \"chat.viewOncePrefix\": \"View once \u00B7 \",\n \"chat.viewOnceTap\": \"View once \u00B7 tap to reveal\",\n \"chat.viewedOnce\": \"Viewed once\",\n \"chat.disappearing\": \"Disappearing message - deletes on both sides\",\n \"chat.disappearAfter\": \"Disappear after\",\n \"chat.visibleFor\": \"Visible for\",\n \"chat.timerPrefix\": \"Timer \u00B7 \",\n \"chat.expired\": \"This message has expired\",\n \"chat.deleteForEveryone\": \"Delete for everyone\",\n \"chat.sending\": \"Sending\",\n \"chat.delivered\": \"Delivered\",\n \"chat.notSent\": \"Not sent\",\n \"chat.notSentReason\": \"Not sent - the secure channel is not ready. Reconnect to continue.\",\n \"chat.encrypted\": \"Encrypted\",\n \"chat.decrypted\": \"Decrypted\",\n \"chat.encryptedOnDevice\": \"Encrypted on your device\",\n \"chat.uploading\": \"Uploading\",\n \"chat.downloading\": \"Downloading\",\n \"chat.transferring\": \"Transferring\u2026\",\n \"chat.copied\": \"Copied!\",\n \"chat.collapse\": \"Collapse\",\n \"chat.closeMenu\": \"Close menu\",\n \"chat.newChat\": \"New chat\",\n \"chat.newGroup\": \"New group\",\n \"chat.createGroup\": \"Create a group\",\n \"chat.groupChats\": \"Group chats\",\n \"chat.secureChat\": \"Secure chat\",\n \"chat.nameThis\": \"Name this chat\",\n \"chat.rename\": \"Rename chat (local only)\",\n \"chat.renameHint\": \"Double-click to rename\",\n \"chat.localLabel\": \"Local label \u00B7 stored only on this device\",\n \"chat.setStatus\": \"Set your status\",\n \"chat.yourStatusPrefix\": \"Your status - \",\n \"chat.meshHint\": \"Up to 8 peers \u00B7 P2P mesh\",\n \"chat.offline\": \"Offline\",\n \"chat.noNetwork\": \"No network \u00B7 reconnecting\",\n \"chat.startVoiceCall\": \"Start encrypted voice call\",\n \"chat.startVideoCall\": \"Start encrypted video call\",\n \"chat.verifyForCalls\": \"Verify the session to enable calls\",\n \"chat.e2eeNote\": \"Every message is end-to-end encrypted on your device before it leaves.\",\n \"chat.peersOnly\": \"Sent end-to-end to connected peers only - never stored on a server.\",\n \"flow.invitationCreated\": \"Secure invitation created\",\n \"flow.invitationCreatedBang\": \"Secure invitation created and encrypted!\",\n \"flow.responseCreated\": \"Secure response created\",\n \"flow.responseCreatedBang\": \"Secure response created!\",\n \"flow.sendInvitation\": \"Send the invitation code to your interlocutor via a secure channel (voice call, SMS, etc.).\",\n \"flow.sendResponse\": \"Send the response code to the initiator via a secure channel or let them scan the QR code below.\",\n \"flow.sendEncryptedCode\": \"Send the encrypted code\",\n \"flow.sendTheResponse\": \"Send the response\",\n \"flow.pasteOrWrite\": \"Paste or write code\u2026\",\n \"flow.processingInvitation\": \"Processing the secure invitation...\",\n \"flow.processingResponse\": \"Processing the secure response...\",\n \"flow.finalizing\": \"Finalizing the secure connection...\",\n \"flow.channelEstablished\": \"Secure channel established\",\n \"flow.channelReady\": \"Secure channel is ready\",\n \"flow.restoring\": \"Restoring connection\u2026\",\n \"flow.invitationCaptured\": \"Invitation captured.\",\n \"err.needInvitation\": \"You need to insert the invitation code from your interlocutor.\",\n \"err.needResponse\": \"You need to insert the response code from your interlocutor.\",\n \"err.responseFormat\": \"Invalid response format - please check the code\",\n \"err.responseNoKey\": \"Invalid response code - missing or corrupted cryptographic key. Please check the code and try again.\",\n \"err.responseNoSignKey\": \"Invalid response code - missing signature verification key. Please check the code and try again.\",\n \"err.responseOutdated\": \"Response data is outdated - please use a fresh invitation\",\n \"err.setupError\": \"Connection setup error\",\n \"err.notReady\": \"Connection not ready\",\n \"err.securityBreach\": \"Security breach detected - connection rejected\",\n \"err.securityValidation\": \"Security validation failed - possible attack detected\",\n \"err.retiredQr\": \"This QR code uses a retired format that could not transfer the invitation. Ask your peer to generate a new one, or use copy/paste.\",\n \"err.compressedQrNote\": \"Compressed QR may omit SDP for brevity. Use copy/paste if connection fails.\",\n \"err.inviteNotConnected\": \"The invitation could not be sent. That chat is not connected right now.\",\n \"err.inviteNotConnectedRetry\": \"The invitation could not be sent. That chat is not connected right now - reopen it and try again.\",\n \"err.nobodyAccepted\": \"Nobody accepted the invitation. The group is unchanged.\",\n \"err.noAudio\": \"No audio captured - check microphone permission and try again.\",\n \"err.voiceNeedsConnection\": \"Voice message needs an active secure connection. Reconnect and try again.\",\n \"err.voiceRestoring\": \"Restoring the connection - try sending the voice message again in a moment.\",\n \"err.fileTooLarge\": \"File too large\",\n \"sas.safetyNumber\": \"Safety number\",\n \"sas.incorrect\": \"Incorrect code. Check it with your peer and try again.\",\n \"sas.noMatch\": \"The codes do not match\",\n \"sas.makeSure\": \"Make sure the codes match exactly.\",\n \"sas.tooManyAttempts\": \"Too many incorrect attempts. Session reset for safety.\",\n \"sas.verificationFailed\": \"Verification failed\",\n \"sas.verified\": \"Verified \u00B7 Perfect Forward Secrecy\",\n \"sas.runVerification\": \"Run security verification\",\n \"sec.panelTitle\": \"Network & crypto details\",\n \"sec.security\": \"Security\",\n \"sec.transport\": \"Transport\",\n \"sec.keyExchange\": \"Key exchange\",\n \"sec.allEnabled\": \"All security features enabled by default\",\n \"sec.realTimeNote\": \"Real-time verification using actual cryptographic functions - no mock data.\",\n \"sec.simulatedWarning\": \"Warning: connection may not be fully established - values may be simulated.\",\n \"dl.title\": \"Download SecureBit\",\n \"dl.free\": \"Free \u00B7 open source\",\n \"dl.soon\": \"Mobile (iOS, Android) and browser extensions (Chrome, Firefox, Opera) are coming soon.\",\n \"chatHdr.chat\": \"Chat\",\n \"chatHdr.chats\": \"Chats\",\n \"chatHdr.newChat\": \"+ New\",\n \"chatHdr.secure\": \"Secure\",\n \"chatHdr.p2pSub\": \"P2P \u00B7 end-to-end encrypted\",\n \"chatHdr.save\": \"Save\",\n \"chatHdr.close\": \"Close\",\n \"chatHdr.expand\": \"Expand\",\n \"chatHdr.you\": \"You\",\n \"chatHdr.qrCode\": \"QR code\",\n \"chat.disconnected\": \"Disconnected\",\n \"call.mute\": \"Mute\",\n \"call.endShort\": \"End\",\n \"group.add\": \"Add\",\n \"group.send\": \"Send\",\n \"sec.pfsShort\": \"Perfect Forward Secrecy\",\n \"sec.cipher\": \"Cipher\",\n \"chat.close\": \"Close\",\n \"file.needConnection\": \"File transfer needs an open connection\",\n \"presence.available\": \"Available\",\n \"presence.away\": \"Away\",\n \"presence.busy\": \"Busy\",\n \"presence.offline\": \"Offline\",\n \"presence.invisible\": \"Invisible\",\n \"presence.availableDesc\": \"Online and reachable\",\n \"presence.awayDesc\": \"Idle \u00B7 stepped away\",\n \"presence.busyDesc\": \"Do not disturb\",\n \"presence.invisibleDesc\": \"Appear offline to peers\",\n \"presence.online\": \"Online\",\n \"conn.p2p\": \"P2P \u00B7 connected\",\n \"conn.verifying\": \"Verifying\u2026\",\n \"conn.connecting\": \"Connecting\u2026\",\n \"conn.reconnecting\": \"Reconnecting\u2026\",\n \"conn.peerDisconnected\": \"Peer disconnected\",\n \"conn.disconnected\": \"Disconnected\",\n \"chat.defaultLabel\": \"Chat\",\n \"groupPhase.forming\": \"Forming\u2026\",\n \"groupPhase.commitments\": \"Exchanging commitments\u2026\",\n \"groupPhase.revealing\": \"Revealing\u2026\",\n \"groupPhase.compare\": \"Compare the group code\",\n \"groupPhase.ready\": \"Group ready\",\n \"groupPhase.failed\": \"Group failed\",\n \"call.qualityGood\": \"Good\",\n \"call.qualityFair\": \"Fair\",\n \"call.qualityWeak\": \"Weak\",\n \"msg.code\": \"Code\",\n \"msg.timer\": \"Timer\",\n \"msg.voice\": \"Voice\",\n \"msg.play\": \"Play\",\n \"msg.sent\": \"Sent\",\n \"msg.read\": \"Read\",\n \"report.title\": \"Real-time security verification\",\n \"report.active\": \"Active\",\n \"report.testsPassed\": \"Tests passed\",\n \"report.verifiedAt\": \"Verified at\",\n \"report.source\": \"Source\",\n \"secTest.verifyECDHKeyExchange\": \"ECDH key exchange\",\n \"secTest.verifyECDSASignatures\": \"ECDSA digital signatures\",\n \"secTest.verifyEncryption\": \"AES-GCM encryption\",\n \"secTest.verifyMessageIntegrity\": \"Message integrity\",\n \"secTest.verifyPerfectForwardSecrecy\": \"Perfect forward secrecy\",\n \"secTest.verifyPFS\": \"Perfect forward secrecy\",\n \"secTest.verifyReplayProtection\": \"Replay protection\",\n \"secTest.verifyDTLSFingerprint\": \"DTLS fingerprint\",\n \"secTest.verifySASVerification\": \"SAS verification\",\n \"secTest.verifyMetadataProtection\": \"Metadata protection\",\n \"secTest.verifyTrafficObfuscation\": \"Traffic obfuscation\",\n \"secTest.verifyPacketPadding\": \"Packet padding\",\n \"secTest.verifyNestedEncryption\": \"Nested encryption\",\n \"secTest.verifyNonExtractableKeys\": \"Non-extractable keys\",\n \"secTest.verifyRateLimiting\": \"Rate limiting\",\n \"secTest.verifyMutualAuth\": \"Mutual authentication\",\n \"secTest.verifyAuthProof\": \"Authentication proof\",\n \"secTest.verifyEnhancedValidation\": \"Enhanced validation\",\n \"secTest.verifyAdvancedFeatures\": \"Advanced features\",\n \"secTest.verifySignature\": \"Signature check\",\n \"secDetail.Security system initializing...\": \"Security system initializing...\",\n \"secDetail.Nested encryption active\": \"Nested encryption active\",\n \"secDetail.Nested encryption failed\": \"Nested encryption failed\",\n \"secDetail.Packet padding active\": \"Packet padding active\",\n \"secDetail.Packet padding failed\": \"Packet padding failed\",\n \"secDetail.Advanced features active\": \"Advanced features active\",\n \"secDetail.Advanced features failed\": \"Advanced features failed\",\n \"secDetail.No encryption key available\": \"No encryption key available\",\n \"secDetail.AES-GCM encryption/decryption working correctly\": \"AES-GCM encryption/decryption working correctly\",\n \"secDetail.No ECDH key pair available\": \"No ECDH key pair available\",\n \"secDetail.Key derivation failed\": \"Key derivation failed\",\n \"secDetail.No ECDSA key pair available\": \"No ECDSA key pair available\",\n \"secDetail.ECDSA digital signatures working correctly\": \"ECDSA digital signatures working correctly\",\n \"secDetail.MAC key not available or invalid\": \"MAC key not available or invalid\",\n \"secDetail.Rate limiter did not block a message over the limit\": \"Rate limiter did not block a message over the limit\",\n \"secDetail.Rate limiter is not available\": \"Rate limiter is not available\",\n \"secDetail.Replay protection is working correctly\": \"Replay protection is working correctly\",\n \"secDetail.Replay protection not enabled\": \"Replay protection not enabled\",\n \"secDetail.SAS code not available\": \"SAS code not available\",\n \"secDetail.SAS verification code is valid and available\": \"SAS verification code is valid and available\",\n \"secDetail.Traffic obfuscation is working correctly\": \"Traffic obfuscation is working correctly\",\n \"secDetail.Traffic obfuscation not enabled\": \"Traffic obfuscation not enabled\",\n \"secDetail.No non-extractable ephemeral ECDH key pair for this session\": \"No non-extractable ephemeral ECDH key pair for this session\",\n \"secDetail.Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation\": \"Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation\",\n \"msg.pause\": \"Pause\",\n \"msg.failed\": \"Failed\",\n \"groupErr.timeout\": \"A member stopped responding before the code was ready.\",\n \"groupErr.keyMismatch\": \"A member's key did not match the identity claimed for it.\",\n \"groupErr.revealMismatch\": \"A member's revealed value did not match what they committed to.\",\n \"groupErr.commitChanged\": \"A member changed their commitment part-way through.\",\n \"groupErr.missingKey\": \"A member was listed whose key never arrived.\",\n \"groupErr.outsider\": \"A frame arrived from someone outside the group.\",\n \"groupErr.limit\": \"A group is limited to eight members.\",\n \"groupErr.tooLarge\": \"A message was too large to send to the group.\",\n \"fileType.text\": \"Plain text\",\n \"fileType.images\": \"Images\",\n \"fileType.archives\": \"Archives\",\n \"fileType.voice\": \"Voice messages\",\n \"fileType.unsupported\": \"Unsupported\",\n \"qrScan.title\": \"Scan QR Code\",\n \"qrScan.auto\": \"Auto Mode\",\n \"qrScan.reset\": \"Reset\",\n \"qrScan.autoScroll\": \"Auto-scrolling enabled\",\n \"qrScan.starting\": \"Starting camera...\",\n \"qrScan.point\": \"Point camera at QR code\",\n \"qrScan.tapFocus\": \"Tap screen to focus\",\n \"qrScan.focusing\": \"Focusing...\",\n \"iceErr.notArray\": \"Server list must be an array\",\n \"iceErr.invalidJson\": \"Invalid JSON\",\n \"iceErr.tooMany\": \"Too many servers (max {max})\",\n \"iceErr.invalidEntry\": \"{label}: invalid entry\",\n \"iceErr.urlCount\": \"{label}: between 1 and {max} URLs required\",\n \"iceErr.turnCreds\": \"{label}: TURN servers usually require a username and credential\",\n \"iceUrl.notString\": \"URL must be a string\",\n \"iceUrl.empty\": \"URL is empty\",\n \"iceUrl.tooLong\": \"URL is too long\",\n \"iceUrl.badChars\": \"URL contains invalid characters\",\n \"iceUrl.badScheme\": \"URL must start with stun:, stuns:, turn: or turns:\",\n \"iceUrl.badQuery\": \"URL has an invalid query\",\n \"iceUrl.noHost\": \"URL is missing a host\",\n \"iceUrl.badHost\": \"URL has an invalid host or port\",\n \"iceUrl.badTransport\": \"URL query must be transport=udp or transport=tcp\",\n \"msg.off\": \"Off\",\n \"file.consentUnavailable\": \"User consent unavailable\",\n \"secLevel.MAXIMUM\": \"MAXIMUM\",\n \"secLevel.INITIALIZING\": \"INITIALIZING\",\n \"secLevel.ERROR\": \"ERROR\",\n \"secLevel.UNKNOWN\": \"UNKNOWN\",\n \"status.error\": \"Error\",\n \"offline.disconnect\": \"Disconnect\",\n \"offline.learnMore\": \"Learn more\",\n \"pwa.installApp\": \"Install App\",\n \"chat.onWeb\": \"You're on Web\",\n \"groupCall.startVoice\": \"Start a group voice call\",\n \"groupCall.startVideo\": \"Start a group video call\",\n \"groupCall.join\": \"Join\",\n \"groupCall.dismiss\": \"Not now\",\n \"groupCall.leave\": \"Leave call\",\n \"groupCall.you\": \"You\",\n \"groupCall.connecting\": \"Connecting\u2026\",\n \"groupCall.waitingLink\": \"Waiting for a direct link\u2026\",\n \"groupCall.legFailed\": \"Could not connect\",\n \"groupCall.startedVoice\": \"{name} started a voice call\",\n \"groupCall.startedVideo\": \"{name} started a video call\",\n \"groupCall.inCall\": \"{count} in the call\",\n \"groupCall.err.permission_denied\": \"The call could not start - microphone and camera access is blocked. Allow it for this site, then try again.\",\n \"groupCall.err.device_not_found\": \"The call could not start - no microphone was found on this device.\",\n \"groupCall.err.device_busy\": \"The call could not start - your microphone is in use by another app. Close it and try again.\",\n \"groupCall.err.media_failed\": \"The call could not start - the microphone could not be opened.\",\n \"groupCall.err.call_in_progress\": \"A call is already running in this group. Join it instead of starting another.\",\n \"groupCall.err.not_ready\": \"Confirm the group code before calling.\",\n \"groupCall.speaking\": \"{name} is speaking\",\n \"groupCall.pin\": \"Show {name} large\",\n \"groupCall.unpin\": \"Back to everyone\",\n \"groupCall.showEveryone\": \"Everyone\",\n \"msg.after5s\": \"5s after reading\",\n \"msg.after15s\": \"15s after reading\",\n \"msg.after30s\": \"30s after reading\",\n \"msg.after1m\": \"1m after reading\",\n \"msg.sec30\": \"30 seconds\",\n \"msg.min5\": \"5 minutes\",\n \"msg.hour1\": \"1 hour\",\n \"desktop.shareDesc\": \"Send the code to your peer. It is one-time and expires shortly.\",\n \"desktop.invitationCreated\": \"Secure invitation created - send it to your peer.\",\n \"desktop.joinDesc\": \"Paste your peer's invitation code to build your secure answer.\",\n \"desktop.offerTag\": \"offer \u00B7 copy text\",\n \"desktop.answerTag\": \"answer \u00B7 copy text\",\n \"desktop.offerPlaceholder\": \"Invitation code will appear here\u2026\",\n \"desktop.answerPlaceholder\": \"Response code will appear here\u2026\",\n \"desktop.backToChats\": \"Back to chats\",\n \"desktop.sendFeedback\": \"Send feedback\",\n \"desktop.callsNeedVerified\": \"Calls require a verified secure channel.\",\n \"desktop.noOpenChannel\": \"No open secure channel.\",\n \"desktop.chatCarriesCall\": \"This chat is carrying a group call. Leave the group call before placing a 1:1 call.\",\n \"desktop.err.permission_denied\": \"The call could not start - microphone and camera access is blocked. Allow it in system settings, then try again.\",\n \"desktop.language\": \"Language\",\n \"desktop.languageHint\": \"Applies at once. Messages already sent keep the words they were written in.\",\n \"desktop.compareCode\": \"Compare the verification code with your peer and confirm if it matches.\",\n \"desktop.verifyBeforeFiles\": \"Complete verification before sending files.\",\n \"desktop.verifyBeforeSending\": \"Complete verification before sending messages.\",\n \"desktop.disconnectedByBackend\": \"Connection securely disconnected.\",\n \"desktop.restoreFailed\": \"Could not restore the connection. Closing this chat.\",\n \"desktop.channelNotReady\": \"The data channel is not available yet. Wait for the connection to establish.\",\n \"desktop.enterCode\": \"Enter the verification code to confirm.\",\n \"desktop.noOffer\": \"No invitation found. Create one first.\",\n \"desktop.notInitiator\": \"You did not create this connection, so there is no answer for you to apply.\",\n \"desktop.decryptFailed\": \"A message could not be decrypted and was discarded.\",\n \"desktop.badMessageFormat\": \"A malformed frame arrived and was discarded.\",\n \"desktop.notSentNotReady\": \"Message not sent: the secure channel is not ready. Wait for verification to finish, or reconnect.\",\n \"desktop.micDenied\": \"Microphone access was denied.\",\n \"desktop.noChannelWait\": \"No open secure channel - wait for the connection to establish.\",\n \"desktop.sasFailed\": \"Verification failed. A machine-in-the-middle may be present, so the connection was aborted.\",\n \"desktop.sasOk\": \"Verification succeeded. This channel is authenticated end to end.\",\n \"desktop.iceForgotten\": \"Saved servers forgotten. Using public ICE servers.\",\n \"desktop.icePublic\": \"Using public ICE servers.\",\n \"desktop.verificationInProgress\": \"Security verification is still running. Wait for it to finish.\",\n \"desktop.fileGone\": \"This file is no longer available for download.\",\n \"desktop.codeMismatch\": \"The verification codes do not match. Disconnecting for safety.\",\n \"desktop.noLocalCode\": \"Verification failed: there is no locally derived code to compare against. Disconnecting for safety.\",\n \"desktop.protocolViolation\": \"The peer claimed the code was confirmed before you confirmed it. Disconnecting for safety.\",\n \"desktop.verificationRejected\": \"Verification rejected. The connection was aborted for safety.\",\n \"secLevel.HIGH\": \"HIGH\",\n \"secLevel.MEDIUM\": \"MEDIUM\",\n \"secLevel.LOW\": \"LOW\",\n \"desktop.rerun\": \"Re-run\",\n \"desktop.ice.customDesc\": \"Up to {max} servers, one URL per line or JSON.\",\n \"desktop.ice.persistDesc\": \"Stored locally on this device. Leave off to use only for this session.\",\n \"desktop.group.leaveNote\": \"The other members are told you left. Nothing about this group is kept on this device afterwards - there is no server holding a copy to rejoin from, so you would need a fresh invitation.\",\n \"desktop.sec.ecdsaOk\": \"Peer key package signature verified during the handshake (ECDSA P-384 / SHA-384)\",\n \"desktop.sec.ecdsaNone\": \"No completed handshake\",\n \"desktop.sec.ecdhOk\": \"Session keys derived from an ephemeral ECDH P-384 exchange via HKDF-SHA-256\",\n \"desktop.sec.ecdhNone\": \"No session keys derived\",\n \"desktop.sec.encryptionOk\": \"AES-GCM encryption and decryption verified inside the core\",\n \"desktop.sec.encryptionNa\": \"The AES-GCM self-test could not be run\",\n \"desktop.sec.integrityOk\": \"Message integrity verified (AES-GCM authentication)\",\n \"desktop.sec.integrityNa\": \"The integrity self-test could not be run\",\n \"desktop.sec.pfsOk\": \"Double Ratchet active - every message has a key of its own\",\n \"desktop.sec.pfsNa\": \"Not measured\",\n \"desktop.sec.replayOk\": \"Ratchet message keys are destroyed once used, so a captured frame cannot be replayed\",\n \"desktop.sec.replayNa\": \"Not measured\",\n \"desktop.sec.dtlsPinned\": \"Peer DTLS fingerprint pinned: {fp}\",\n \"desktop.sec.dtlsNone\": \"No remote DTLS fingerprint available\",\n \"desktop.sec.sasBoth\": \"Both peers confirmed the out-of-band safety code\",\n \"desktop.sec.sasIncomplete\": \"The safety code has not been confirmed by both peers\",\n \"desktop.sec.metadataOk\": \"A dedicated metadata key was derived (HKDF metadata-protection-v4)\",\n \"desktop.sec.metadataNone\": \"No metadata key\",\n \"desktop.sec.coverOn\": \"Cover traffic active: random fake frames (32-128 B every 15-30 s) on the ratcheted channel\",\n \"desktop.sec.coverOff\": \"The cover traffic generator is not running\",\n \"desktop.sec.coverUnsupported\": \"Cover traffic needs a Double Ratchet session (the peer is on an older release)\",\n \"desktop.tagline\": \"End-to-end freedom\",\n \"desktop.callsNotHere\": \"Calls are not available in this build yet. Messages, files and group chats work as usual.\"\n};\n\nconst registry = globalThis.__SECUREBIT_I18N__ || (globalThis.__SECUREBIT_I18N__ = Object.create(null));\nregistry[\"en\"] = DICTIONARY;\n", "/**\n * Language selection and string lookup.\n *\n * Every locale is a real page at a real URL (/, /de/, ...), generated at build time,\n * because the app is client-rendered: a language that exists only as a runtime string\n * swap has no URL for a crawler to index. This module is the runtime half \u2014 it decides\n * which locale the current page is, and hands components their strings.\n *\n * The rule that matters: the URL wins over everything. Someone who opens /de/ gets\n * German even if they once chose English here, or a shared link would open in whatever\n * language the recipient happened to pick last, which makes links unshareable.\n */\n\nimport { DEFAULT_LOCALE, SUPPORTED_LOCALES, LOCALE_META, CROSS_LOCALE_STRINGS } from './generated.js';\n// Side-effect import: loading the default locale's dictionary registers the strings\n// t() falls back to when a translation is missing a key. It is the one dictionary that\n// has to be present on every page, so it is the one that gets bundled.\nimport './dict/default.js';\n\nexport { DEFAULT_LOCALE, SUPPORTED_LOCALES, LOCALE_META };\n\n/**\n * Loaded dictionaries, by locale code.\n *\n * On a global rather than in module scope, and that is the point. index.js exists twice\n * on a page: once bundled inside dist/app.js, and once as raw source, because the PWA\n * install prompt, the PWA manager and the update checker import it directly. Two module\n * instances mean two module-scoped registries, and the /ru/ dictionary loaded by the\n * page would have been invisible to the half of the app that needed it.\n *\n * A page loads its own dictionary through a
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
@@ -116,7 +116,7 @@
-
+
@@ -125,7 +125,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+