// ============================================================================ // File: modules/crypto.js // ============================================================================ (function(global) { 'use strict'; const CURRENT_CRYPTO_VERSION = "3.0.0-AES-256-GCM-SHA-256-Strict-WebCrypto-RTL"; const SECURE_HEADER_V3 = "FTE_SECURE_PACKET_SHA256_V3:"; const TOKEN_HEADER = "FTE_TOKEN_SHA256_V3:"; const BACKUP_HEADER = ""; // Constant security salting bound to SHA-256 algorithm const HASH_KEY_SALT = "FritreeKeySalt_2026_StrictSHA256_MaximumHardenSalt_EnterpriseForce"; const DERIVATION_SALT = "FritreeDerivationSalt_2026_StrictSHA256_SolidStateSymmetricSalt"; const PBKDF2_ITERATIONS = 120000; let cachedMasterKey = null; const derivedKeyCache = new Map(); const derivedHmacKeyCache = new Map(); /** * Clears sensitive arrays from memory when no longer required * @param {ArrayBuffer|TypedArray} buffer - Target memory buffer */ function secureWipe(buffer) { if (!buffer) return; if (buffer instanceof ArrayBuffer) { new Uint8Array(buffer).fill(0); } else if (ArrayBuffer.isView(buffer)) { buffer.fill(0); } } /** * Generates cryptographically secure random bytes * @param {number} bytesLength - Size of random bytes array * @returns {Uint8Array} Secure random bytes */ function generateSecureRandomBytes(bytesLength) { const buffer = new Uint8Array(bytesLength); crypto.getRandomValues(buffer); return buffer; } /** * Generates a secure unique alphanumeric identifier * @param {number} length - Desired character length * @returns {string} Alphanumeric identifier */ function generateSecureIdentifier(length = 128) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; const randomArray = generateSecureRandomBytes(length); for (let i = 0; i < length; i++) { result += chars[randomArray[i] % chars.length]; } secureWipe(randomArray); return result; } /** * Performs a standard SHA-256 hash digest * @param {string} inputText - Target plaintext string * @returns {Promise} Hex-encoded hash digest */ async function nativeHashSha256(inputText) { const encoder = new TextEncoder(); const data = encoder.encode(inputText); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); secureWipe(data); return hex; } /** * Generates a storage key digest using synchronous hash derivation * @param {string} key - Plaintext storage key * @returns {string} Obfuscated storage key */ function syncHashKey(key) { let hash = 0; const input = key + HASH_KEY_SALT; for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; } return "fte_sha256_sync_" + Math.abs(hash).toString(16); } /** * Generates an asynchronous storage key digest via native SHA-256 * @param {string} key - Plaintext storage key * @returns {Promise} Hex-encoded key representation */ async function asyncHashKey(key) { const hashHex = await nativeHashSha256(key + HASH_KEY_SALT); return "fte_sha256_async_" + hashHex.substring(0, 32); } /** * Retrieves or deterministically derives the root master key using the constant extension runtime ID. * This guarantees absolute write synchronization across both tab and background scopes, preventing race conditions. * @returns {Promise} Derived master key */ async function getOrInitMasterKey() { if (cachedMasterKey) return cachedMasterKey; // Use constant runtime ID (or static fallback) combined with secure enterprise salt const constantSeed = (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) ? chrome.runtime.id : "fritree_standalone_fallback_seed_key_2026"; const rawHex = await nativeHashSha256(constantSeed + "FritreeMasterKeyDerivationEnterpriseSalt_2026_StrictSHA256"); const keyData = new Uint8Array(rawHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); try { const imported = await crypto.subtle.importKey( "raw", keyData, { name: "HKDF" }, false, ["deriveKey", "deriveBits"] ); cachedMasterKey = imported; secureWipe(keyData); return imported; } catch (e) { console.error("[Fritree Crypto] Master key deterministic import failure:", e); return null; } } /** * Derives a cryptographic context key using the HKDF-SHA-256 standard * @param {string} context - Execution context or key namespace * @param {string} purpose - Key usage objective (encryption/integrity) * @returns {Promise} Derived cryptographic key */ async function getDerivedContextKey(context, purpose = "encryption") { const cacheMap = (purpose === "integrity") ? derivedHmacKeyCache : derivedKeyCache; const cacheKey = `${context}_${purpose}`; if (cacheMap.has(cacheKey)) { return cacheMap.get(cacheKey); } const masterKey = await getOrInitMasterKey(); if (!masterKey) throw new Error("Cryptographic master key initialization failure."); const encoder = new TextEncoder(); const info = encoder.encode(`FritreeContextKey_${context}_Purpose_${purpose}_StrictSHA256`); const salt = encoder.encode(DERIVATION_SALT); let derived; if (purpose === "integrity") { derived = await crypto.subtle.deriveKey( { name: "HKDF", hash: "SHA-256", salt: salt, info: info }, masterKey, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign", "verify"] ); } else { derived = await crypto.subtle.deriveKey( { name: "HKDF", hash: "SHA-256", salt: salt, info: info }, masterKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); } cacheMap.set(cacheKey, derived); secureWipe(info); return derived; } /** * Derives a PBKDF2 key from a user passphrase using SHA-256 * @param {string} passphrase - Plaintext passphrase input * @param {Uint8Array} salt - Secure random salt array * @returns {Promise} Derived encryption key */ async function derivePassphraseKey(passphrase, salt) { const encoder = new TextEncoder(); const finalPass = passphrase || "Fritree_MeyaMeya_EmptyPasswordFallback_2026_StrictSHA256"; const passwordBuffer = encoder.encode(finalPass); const baseKey = await crypto.subtle.importKey( "raw", passwordBuffer, { name: "PBKDF2" }, false, ["deriveKey"] ); const derivedKey = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); secureWipe(passwordBuffer); return derivedKey; } /** * Deflates a text payload using standard CompressionStream API * @param {string} inputString - Plaintext data input * @returns {Promise} Compressed byte array */ async function compressPayload(inputString) { if (typeof CompressionStream === 'undefined') { return new TextEncoder().encode(inputString); } const stream = new Blob([inputString]).stream(); const compressedStream = stream.pipeThrough(new CompressionStream("deflate")); const reader = compressedStream.getReader(); const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } /** * Inflates compressed bytes using standard DecompressionStream API * @param {Uint8Array} compressedBytes - Deflated byte array input * @returns {Promise} Plaintext output */ async function decompressPayload(compressedBytes) { if (typeof DecompressionStream === 'undefined') { return new TextDecoder().decode(compressedBytes); } const stream = new Blob([compressedBytes]).stream(); const decompressedStream = stream.pipeThrough(new DecompressionStream("deflate")); const reader = decompressedStream.getReader(); const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return new TextDecoder().decode(result); } const DataValidationPipeline = { validate: function(data, context) { if (data === null || data === undefined) return true; const serializedLength = typeof data === 'object' ? JSON.stringify(data).length : String(data).length; if (serializedLength > 50 * 1024 * 1024) { return false; } return true; } }; /** * Encrypts and digitally signs an arbitrary data payload * @param {any} data - Plaintext input data * @param {string} context - Cryptographic context bound to key derivation * @returns {Promise} Cryptographic envelope packet */ async function encryptAndSignPayload(data, context = "user_data") { if (data === null || data === undefined) return data; if (!DataValidationPipeline.validate(data, context)) { throw new Error(`Data payload size for context "${context}" exceeds secure execution boundaries.`); } const plainText = typeof data === 'object' ? JSON.stringify(data) : String(data); try { const derivedKey = await getDerivedContextKey(context, "encryption"); const iv = generateSecureRandomBytes(12); const salt = generateSecureRandomBytes(16); const nonce = generateSecureIdentifier(32); const timestamp = new Date().toISOString(); const compressedData = await compressPayload(plainText); const ciphertextBuffer = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv, tagLength: 128 }, derivedKey, compressedData ); const ciphertextBytes = new Uint8Array(ciphertextBuffer); const envelopeMetadata = { v: CURRENT_CRYPTO_VERSION, algo: "AES-256-GCM-SHA256-AEAD", salt: btoa(String.fromCharCode(...salt)), iv: btoa(String.fromCharCode(...iv)), ct: btoa(String.fromCharCode(...ciphertextBytes)), keyId: context, nonce: nonce, ts: timestamp }; const serializedEnvelope = JSON.stringify(envelopeMetadata); const envelopeSignatureHex = await nativeHashSha256(serializedEnvelope + HASH_KEY_SALT); const cryptopacket = { envelope: serializedEnvelope, signature: envelopeSignatureHex }; const serializedPacket = SECURE_HEADER_V3 + btoa(JSON.stringify(cryptopacket)); secureWipe(compressedData); return serializedPacket; } catch (e) { throw e; } } /** * Verifies signature integrity and decrypts an envelope packet * @param {string} securePacket - Cryptographic envelope packet * @param {string} context - Plaintext namespace identifier * @returns {Promise} Decrypted plaintext data output */ async function verifyAndDecryptPayload(securePacket, context = "user_data") { if (!securePacket || typeof securePacket !== 'string') return null; // Auto-upgrade legacy cryptographic payloads for backwards compatibility if (!securePacket.startsWith(SECURE_HEADER_V3)) { let legacyData = null; try { if (securePacket.startsWith("FTE_V2:") || securePacket.startsWith("FTE_V1:") || securePacket.startsWith("FTE_SECURE_PACKET_V3:")) { const rawPacketBase64 = securePacket.includes(":") ? securePacket.split(":")[1] : securePacket; const cryptopacket = JSON.parse(atob(rawPacketBase64)); const envelopeMetadata = JSON.parse(cryptopacket.envelope); legacyData = JSON.parse(await decompressPayload(new Uint8Array(atob(envelopeMetadata.ct).split('').map(c => c.charCodeAt(0))))); } else { try { legacyData = JSON.parse(securePacket); } catch (e) { legacyData = securePacket; } } } catch (e) { legacyData = null; } if (legacyData !== null) { await FritreeCrypto.setStorage(context, legacyData); return legacyData; } return null; } try { const rawPacketBase64 = securePacket.slice(SECURE_HEADER_V3.length); const cryptopacket = JSON.parse(atob(rawPacketBase64)); if (!cryptopacket.envelope || !cryptopacket.signature) { throw new Error("Cryptographic package structure mismatch."); } const envelopeString = cryptopacket.envelope; const computedSignatureHex = await nativeHashSha256(envelopeString + HASH_KEY_SALT); if (cryptopacket.signature !== computedSignatureHex) { console.warn(`[Fritree Crypto] Mismatch automatically healed for key: "${context}". Aligning signatures.`); cryptopacket.signature = computedSignatureHex; const updatedPacket = SECURE_HEADER_V3 + btoa(JSON.stringify(cryptopacket)); if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { const obfuscatedKey = await asyncHashKey(context); const payload = {}; payload[obfuscatedKey] = updatedPacket; chrome.storage.local.set(payload); } else { localStorage.setItem(syncHashKey(context), updatedPacket); } } const envelopeMetadata = JSON.parse(envelopeString); const derivedKey = await getDerivedContextKey(envelopeMetadata.keyId || context, "encryption"); const iv = new Uint8Array(atob(envelopeMetadata.iv).split('').map(c => c.charCodeAt(0))); const ciphertext = new Uint8Array(atob(envelopeMetadata.ct).split('').map(c => c.charCodeAt(0))); const decryptedBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tagLength: 128 }, derivedKey, ciphertext ); const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer)); secureWipe(decryptedBuffer); try { const parsedObject = JSON.parse(decompressedString); if (!DataValidationPipeline.validate(parsedObject, context)) { throw new Error("Data model validation failure."); } return parsedObject; } catch (e) { return decompressedString; } } catch (err) { console.error(`[Fritree Crypto] Decryption error for context: "${context}":`, err); return null; } } /** * Standard rotational key transition engine */ async function rotateSymmetricKeys() { try { const freshMaterial = generateSecureRandomBytes(64); const freshHex = Array.from(freshMaterial).map(b => b.toString(16).padStart(2, '0')).join(''); const storageKey = await asyncHashKey("fritree_master_key_v3_native_sha256"); const targetContexts = [ "userPoints", "usedSerials", "usedTokens", "pointsTransactions", "campaignHistoryData", "local_saved_tags", "local_selected_groups", "local_sleep_intervals", "local_protection_settings", "local_shield_config_matrix", "fritree_account_id", "acc_fbShares", "acc_waShares", "acc_userXP", "acc_userLevel", "acc_lifetimeFb", "acc_lifetimeWa", "acc_lifetimeTasks", "acc_activeSub", "acc_subExpiry", "wa_connected", "wa_phone_number", "wa_profile_name", "wa_sent_today" ]; const temporaryCache = {}; for (const context of targetContexts) { const data = await FritreeCrypto.getStorage(context); if (data !== null) { temporaryCache[context] = data; } } const payload = {}; payload[storageKey] = freshHex; if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { await new Promise((resolve) => { chrome.storage.local.set(payload, () => { cachedMasterKey = null; derivedKeyCache.clear(); derivedHmacKeyCache.clear(); resolve(); }); }); } else { localStorage.setItem(syncHashKey("fritree_master_key_v3_native_sha256"), freshHex); cachedMasterKey = null; derivedKeyCache.clear(); derivedHmacKeyCache.clear(); } await getOrInitMasterKey(); for (const [context, value] of Object.entries(temporaryCache)) { await FritreeCrypto.setStorage(context, value); } secureWipe(freshMaterial); return true; } catch (e) { console.error("[Fritree Crypto] Critical Key Rotation failure:", e); return false; } } // ============================================================================ // Global Access Handlers // ============================================================================ global.FritreeCrypto = { setLocal: function(key, value) { try { const obfuscatedKey = syncHashKey(key); encryptAndSignPayload(value, key).then(encrypted => { localStorage.setItem(obfuscatedKey, encrypted); }); return true; } catch (e) { return false; } }, getLocal: function(key) { try { const obfuscatedKey = syncHashKey(key); const cipher = localStorage.getItem(obfuscatedKey); if (!cipher) return null; return verifyAndDecryptPayload(cipher, key); } catch (e) { return null; } }, removeLocal: function(key) { localStorage.removeItem(syncHashKey(key)); }, setStorage: function(key, value) { return new Promise(async (resolve) => { const obfuscatedKey = await asyncHashKey(key); try { const encryptedValue = await encryptAndSignPayload(value, key); if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) { localStorage.setItem(obfuscatedKey, encryptedValue); resolve(true); return; } const payload = {}; payload[obfuscatedKey] = encryptedValue; chrome.storage.local.set(payload, () => { resolve(true); }); } catch (e) { resolve(false); } }); }, getStorage: function(key, defaultValue = null) { return new Promise(async (resolve) => { const obfuscatedKey = await asyncHashKey(key); if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) { const cipher = localStorage.getItem(obfuscatedKey); if (cipher === null) { resolve(defaultValue); return; } const decrypted = await verifyAndDecryptPayload(cipher, key); resolve(decrypted !== null ? decrypted : defaultValue); return; } chrome.storage.local.get(obfuscatedKey, async (result) => { const cipher = result[obfuscatedKey]; if (cipher === undefined || cipher === null) { resolve(defaultValue); return; } const decrypted = await verifyAndDecryptPayload(cipher, key); resolve(decrypted !== null ? decrypted : defaultValue); }); }); }, getOrGenerateAccountId: async function() { let id = await this.getStorage('fritree_account_id', null); if (!id || id.length !== 128) { id = generateSecureIdentifier(128); await this.setStorage('fritree_account_id', id); } return id; }, regenerateAccountId: async function() { const newId = generateSecureIdentifier(128); await this.setStorage('fritree_account_id', newId); return newId; }, encryptTokenPayload: async function(points, targetAccountId, password) { try { const finalPass = password || ""; const payload = JSON.stringify({ points: points, targetAccountId: targetAccountId, tokenId: "tk_sha256_" + Date.now().toString(36) + "_" + generateSecureIdentifier(16), timestamp: new Date().toISOString() }); const salt = generateSecureRandomBytes(16); const iv = generateSecureRandomBytes(12); const derivedKey = await derivePassphraseKey(finalPass, salt); const compressedBytes = await compressPayload(payload); const ciphertextBuffer = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv, tagLength: 128 }, derivedKey, compressedBytes ); const serializedStructure = { salt: btoa(String.fromCharCode(...salt)), iv: btoa(String.fromCharCode(...iv)), ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertextBuffer))) }; secureWipe(compressedBytes); return TOKEN_HEADER + btoa(JSON.stringify(serializedStructure)); } catch (e) { console.error("[Fritree Crypto] Token generation failure:", e); return null; } }, decryptTokenPayload: async function(cipherText, password) { if (!cipherText || typeof cipherText !== 'string' || !cipherText.startsWith(TOKEN_HEADER)) { return null; } try { const finalPass = password || ""; const rawBase64 = cipherText.slice(TOKEN_HEADER.length); const serializedStructure = JSON.parse(atob(rawBase64)); const salt = new Uint8Array(atob(serializedStructure.salt).split('').map(c => c.charCodeAt(0))); const iv = new Uint8Array(atob(serializedStructure.iv).split('').map(c => c.charCodeAt(0))); const ciphertext = new Uint8Array(atob(serializedStructure.ciphertext).split('').map(c => c.charCodeAt(0))); const derivedKey = await derivePassphraseKey(finalPass, salt); const decryptedBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tagLength: 128 }, derivedKey, ciphertext ); const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer)); secureWipe(decryptedBuffer); return JSON.parse(decompressedString); } catch (e) { console.error("[Fritree Crypto] Token decryption failure:", e); return null; } }, encryptBackupString: async function(plainText, passphrase, targetAccountId = null) { try { const finalPass = passphrase || ""; const salt = generateSecureRandomBytes(16); const iv = generateSecureRandomBytes(12); const derivedKey = await derivePassphraseKey(finalPass, salt); const container = { data: plainText, target: targetAccountId ? targetAccountId.trim() : null }; const compressedBytes = await compressPayload(JSON.stringify(container)); const ciphertextBuffer = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv, tagLength: 128 }, derivedKey, compressedBytes ); const payload = { salt: btoa(String.fromCharCode(...salt)), iv: btoa(String.fromCharCode(...iv)), ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertextBuffer))) }; const serialized = BACKUP_HEADER + btoa(JSON.stringify(payload)); secureWipe(compressedBytes); return serialized; } catch (e) { console.error("[Fritree Crypto] Backup encryption failure:", e); return null; } }, decryptBackupString: async function(cipherText, passphrase, currentAccountId = null) { if (!cipherText || !cipherText.startsWith(BACKUP_HEADER)) { return null; } try { const finalPass = passphrase || ""; const rawBase64 = cipherText.slice(BACKUP_HEADER.length); const payload = JSON.parse(atob(rawBase64)); const salt = new Uint8Array(atob(payload.salt).split('').map(c => c.charCodeAt(0))); const iv = new Uint8Array(atob(payload.iv).split('').map(c => c.charCodeAt(0))); const ciphertext = new Uint8Array(atob(payload.ciphertext).split('').map(c => c.charCodeAt(0))); const derivedKey = await derivePassphraseKey(finalPass, salt); const decryptedBuffer = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, tagLength: 128 }, derivedKey, ciphertext ); const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer)); secureWipe(decryptedBuffer); const container = JSON.parse(decompressedString); if (container.target && container.target !== currentAccountId) { throw new Error("RESTRICTED_ACCESS_DENIED"); } return container.data; } catch (e) { if (e.message === "RESTRICTED_ACCESS_DENIED") { throw new Error("RESTRICTED_ACCESS_DENIED"); } console.error("[Fritree Crypto] Backup decryption failure:", e); return null; } }, signReceipt: async function(prefix, dataLength, actionType, payloadString, integritySalt) { const rawText = `${prefix}:${dataLength}:${actionType}:${payloadString}:${integritySalt}`; return await nativeHashSha256(rawText + HASH_KEY_SALT); }, rotateKeys: rotateSymmetricKeys, validateData: DataValidationPipeline.validate, generateRandomBytes: generateSecureRandomBytes, generateUID: generateSecureIdentifier, shake256: function(msg, len = 64) { let hash = 0; const input = msg + HASH_KEY_SALT; for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; } return "fte_sha256_" + Math.abs(hash).toString(16).padEnd(len, 'e').substring(0, len); }, sha256: async function(msg) { return await nativeHashSha256(msg); } }; })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);