Download modules/crypto.js from Althnayi/facebook: direct link, hf CLI and curl.
- Browser
- Download file 31.6 kB
-
https://huggingface.co/spaces/Althnayi/facebook/resolve/main/modules/crypto.js
- Command line
-
hf download hf://spaces/Althnayi/facebook/modules/crypto.js
-
curl -L -o crypto.js https://huggingface.co/spaces/Althnayi/facebook/resolve/main/modules/crypto.js
31.6 kB
| // ============================================================================ | |
| // 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<string>} 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<string>} 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<CryptoKey>} 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<CryptoKey>} 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<CryptoKey>} 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<Uint8Array>} 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<string>} 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<string>} 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<any>} 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); |