| |
| |
| |
| |
|
|
|
|
| (global => {
|
| 'use strict';
|
|
|
| const CURRENT_CRYPTO_VERSION = "5.0-EnterpriseFullE2E";
|
| const SECURE_HEADER = "";
|
| const TOKEN_HEADER = "";
|
| const BACKUP_HEADER = "";
|
| const CAPSULE_HEADER = "";
|
|
|
| const HASH_KEY_SALT = "FritreeStorageKeyObfuscationSalt_2026_StrictSHA256_AEADMasterLock";
|
| const DERIVATION_SALT = "FritreeMasterKeyDerivationEnterpriseSalt_2026_StrictSHA256";
|
| const INTEGRITY_SEAL_SALT = "FritreeIntegritySealValidationSalt_SHA256_2026_EnterpriseSecureForce";
|
| const PBKDF2_ITERATIONS = 200000;
|
|
|
| let cachedMasterKey = null;
|
| const derivedKeyCache = new Map();
|
| const derivedHmacKeyCache = new Map();
|
|
|
| |
| |
|
|
| function silentTrap(err) {
|
| if (err && console && console.warn) {
|
| console.warn("[Fritree Crypto Core SafeTrap]", err);
|
| }
|
| return null;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function uint8ArrayToBase64(uint8) {
|
| try {
|
| if (!uint8 || !(uint8 instanceof Uint8Array)) return '';
|
| let binary = '';
|
| const len = uint8.byteLength;
|
| const chunk = 8192;
|
| for (let i = 0; i < len; i += chunk) {
|
| const slice = uint8.subarray(i, i + chunk);
|
| binary += String.fromCharCode.apply(null, slice);
|
| }
|
| return btoa(binary);
|
| } catch (e) {
|
| return silentTrap(e) || '';
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function base64ToUint8Array(base64) {
|
| try {
|
| if (!base64 || typeof base64 !== 'string') return new Uint8Array(0);
|
| const rawBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
| const binary = atob(rawBase64);
|
| const len = binary.length;
|
| const bytes = new Uint8Array(len);
|
| for (let i = 0; i < len; i++) {
|
| bytes[i] = binary.charCodeAt(i);
|
| }
|
| return bytes;
|
| } catch (e) {
|
| silentTrap(e);
|
| return new Uint8Array(0);
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| function secureMemoryWipe(buffer) {
|
| try {
|
| if (!buffer) return;
|
| if (buffer instanceof ArrayBuffer) {
|
| new Uint8Array(buffer).fill(0);
|
| } else if (ArrayBuffer.isView(buffer)) {
|
| buffer.fill(0);
|
| }
|
| } catch (e) {
|
| silentTrap(e);
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function generateSecureRandomBytes(bytesLength) {
|
| try {
|
| const buffer = new Uint8Array(bytesLength);
|
| crypto.getRandomValues(buffer);
|
| return buffer;
|
| } catch (e) {
|
| silentTrap(e);
|
| const fb = new Uint8Array(bytesLength);
|
| for (let i = 0; i < bytesLength; i++) {
|
| fb[i] = Math.floor(Math.random() * 256);
|
| }
|
| return fb;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function generateSecureIdentifier(length = 128) {
|
| try {
|
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
| let result = '';
|
| const randomArray = generateSecureRandomBytes(length);
|
| for (let i = 0; i < length; i++) {
|
| result += chars[randomArray[i] % chars.length];
|
| }
|
| secureMemoryWipe(randomArray);
|
| return result;
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fteFallbackId" + Math.random().toString(36).substring(2) + "0".repeat(90);
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| async function nativeHashSha256(inputText) {
|
| try {
|
| const encoder = new TextEncoder();
|
| const data = encoder.encode(String(inputText !== undefined && inputText !== null ? 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('');
|
| secureMemoryWipe(data);
|
| return hex;
|
| } catch (e) {
|
| silentTrap(e);
|
| return "0000000000000000000000000000000000000000000000000000000000000000";
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function syncHashKey(key) {
|
| try {
|
| let hash = 0;
|
| const input = String(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_sh_v5_" + Math.abs(hash).toString(16);
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fallback_sync_" + String(key).length;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| async function asyncHashKey(key) {
|
| try {
|
| const hashHex = await nativeHashSha256(String(key) + HASH_KEY_SALT);
|
| return "fte_sh_v5_" + hashHex.substring(0, 32);
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fallback_async_" + String(key).length;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function getOrInitMasterKey() {
|
| try {
|
| if (cachedMasterKey) return cachedMasterKey;
|
| const constantSeed = (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id)
|
| ? chrome.runtime.id
|
| : "fritree_standalone_fallback_seed_key_2026_e2e_v5";
|
|
|
| const rawHex = await nativeHashSha256(constantSeed + DERIVATION_SALT);
|
| const keyData = new Uint8Array(rawHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
|
|
| const imported = await crypto.subtle.importKey(
|
| "raw",
|
| keyData,
|
| { name: "HKDF" },
|
| false,
|
| ["deriveKey", "deriveBits"]
|
| );
|
|
|
| cachedMasterKey = imported;
|
| secureMemoryWipe(keyData);
|
| return imported;
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| async function getDerivedContextKey(context, purpose = "encryption") {
|
| try {
|
| const cacheMap = (purpose === "integrity") ? derivedHmacKeyCache : derivedKeyCache;
|
| const cacheKey = `${context}_${purpose}`;
|
| if (cacheMap.has(cacheKey)) {
|
| return cacheMap.get(cacheKey);
|
| }
|
|
|
| const masterKey = await getOrInitMasterKey();
|
| if (!masterKey) return null;
|
|
|
| const encoder = new TextEncoder();
|
| const info = encoder.encode(`FritreeContextKey_${context}_Purpose_${purpose}_V5_AEAD`);
|
| 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);
|
| secureMemoryWipe(info);
|
| return derived;
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function derivePassphraseKey(passphrase, salt) {
|
| try {
|
| const encoder = new TextEncoder();
|
| const finalPass = passphrase || "Fritree_Default_Secure_Passphrase_V5_StrictAEAD";
|
| 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"]
|
| );
|
|
|
| secureMemoryWipe(passwordBuffer);
|
| return derivedKey;
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function compressPayload(inputString) {
|
| try {
|
| const encoder = new TextEncoder();
|
| const inputData = encoder.encode(inputString);
|
|
|
| if (typeof CompressionStream === 'undefined') {
|
| return inputData;
|
| }
|
|
|
| const stream = new Blob([inputData]).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;
|
| } catch (e) {
|
| silentTrap(e);
|
| return new TextEncoder().encode(inputString);
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function decompressPayload(compressedBytes) {
|
| try {
|
| 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);
|
| } catch (e) {
|
| silentTrap(e);
|
| return new TextDecoder().decode(compressedBytes);
|
| }
|
| }
|
|
|
| |
| |
|
|
| const DataValidationPipeline = {
|
| validate: function(data, context) {
|
| try {
|
| if (data === null || data === undefined) return true;
|
| const serializedLength = typeof data === 'object' ? JSON.stringify(data).length : String(data).length;
|
| if (serializedLength > 150 * 1024 * 1024) {
|
| return false;
|
| }
|
| return true;
|
| } catch (e) {
|
| silentTrap(e);
|
| return false;
|
| }
|
| }
|
| };
|
|
|
| |
| |
| |
| |
| |
|
|
| async function encryptAndSignPayload(data, context = "user_data") {
|
| try {
|
| if (!DataValidationPipeline.validate(data, context)) {
|
| return null;
|
| }
|
|
|
| const plainText = JSON.stringify(data !== undefined ? data : null);
|
| const derivedKey = await getDerivedContextKey(context, "encryption");
|
| if (!derivedKey) return null;
|
|
|
| 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: uint8ArrayToBase64(salt),
|
| iv: uint8ArrayToBase64(iv),
|
| ct: uint8ArrayToBase64(ciphertextBytes),
|
| keyId: context,
|
| nonce: nonce,
|
| ts: timestamp
|
| };
|
|
|
| const serializedEnvelope = JSON.stringify(envelopeMetadata);
|
| const envelopeSignatureHex = await nativeHashSha256(serializedEnvelope + INTEGRITY_SEAL_SALT);
|
|
|
| const cryptopacket = {
|
| envelope: serializedEnvelope,
|
| signature: envelopeSignatureHex
|
| };
|
|
|
| const serializedPacket = SECURE_HEADER + btoa(JSON.stringify(cryptopacket));
|
| secureMemoryWipe(compressedData);
|
| return serializedPacket;
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| async function verifyAndDecryptPayload(securePacket, context = "user_data") {
|
| try {
|
| if (!securePacket || typeof securePacket !== 'string') return null;
|
|
|
| if (!securePacket.startsWith(SECURE_HEADER)) {
|
| let legacyData = null;
|
| try {
|
| if (securePacket.startsWith("") || securePacket.startsWith("FTE:") || securePacket.startsWith("FTE_ENC_V4_GCM_AEAD:")) {
|
| 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(base64ToUint8Array(envelopeMetadata.ct)));
|
| } else {
|
| try { legacyData = JSON.parse(securePacket); } catch (e) { legacyData = securePacket; }
|
| }
|
| } catch (e) {
|
| silentTrap(e);
|
| legacyData = null;
|
| }
|
|
|
| if (legacyData !== null) {
|
| await global.FritreeCrypto.setStorage(context, legacyData);
|
| return legacyData;
|
| }
|
| return null;
|
| }
|
|
|
| const rawPacketBase64 = securePacket.slice(SECURE_HEADER.length);
|
| const cryptopacket = JSON.parse(atob(rawPacketBase64));
|
|
|
| if (!cryptopacket.envelope || !cryptopacket.signature) {
|
| console.warn("[Fritree Crypto] Rejected malformed or tampered packet for context:", context);
|
| return null;
|
| }
|
|
|
| const envelopeString = cryptopacket.envelope;
|
| const computedSignatureHex = await nativeHashSha256(envelopeString + INTEGRITY_SEAL_SALT);
|
|
|
| if (cryptopacket.signature !== computedSignatureHex) {
|
| console.error("[Fritree Crypto] TAMPER DETECTED in storage element:", context, "- Rejecting modified value.");
|
| return null;
|
| }
|
|
|
| const envelopeMetadata = JSON.parse(envelopeString);
|
| const derivedKey = await getDerivedContextKey(envelopeMetadata.keyId || context, "encryption");
|
| if (!derivedKey) return null;
|
|
|
| const iv = base64ToUint8Array(envelopeMetadata.iv);
|
| const ciphertext = base64ToUint8Array(envelopeMetadata.ct);
|
|
|
| const decryptedBuffer = await crypto.subtle.decrypt(
|
| { name: "AES-GCM", iv: iv, tagLength: 128 },
|
| derivedKey,
|
| ciphertext
|
| );
|
|
|
| const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer));
|
| secureMemoryWipe(decryptedBuffer);
|
|
|
| try {
|
| const parsedObject = JSON.parse(decompressedString);
|
| if (!DataValidationPipeline.validate(parsedObject, context)) {
|
| return null;
|
| }
|
| return parsedObject;
|
| } catch (e) {
|
| return decompressedString;
|
| }
|
| } catch (err) {
|
| silentTrap(err);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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_v5_native_sha256");
|
|
|
| const targetContexts = [
|
| "usdPoints", "usedSerials", "usedTokens", "pointsTransactions",
|
| "campaignHistoryData", "local_saved_tags", "local_selected_groups",
|
| "local_sleep_intervals", "local_protection_settings", "local_shield_config_matrix",
|
| "fritree_account_id", "acc_fbPoints", "acc_waPoints", "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", "local_previous_posts", "local_time_capsules",
|
| "local_contacts_database", "local_calendar_events_db", "local_post_groups"
|
| ];
|
|
|
| const temporaryCache = {};
|
| for (const context of targetContexts) {
|
| const data = await global.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_v5_native_sha256"), freshHex);
|
| cachedMasterKey = null;
|
| derivedKeyCache.clear();
|
| derivedHmacKeyCache.clear();
|
| }
|
|
|
| await getOrInitMasterKey();
|
|
|
| for (const [context, value] of Object.entries(temporaryCache)) {
|
| await global.FritreeCrypto.setStorage(context, value);
|
| }
|
|
|
| secureMemoryWipe(freshMaterial);
|
| return true;
|
| } catch (e) {
|
| silentTrap(e);
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function encryptConditionalCapsulePayload(capsuleData, passphrase = "") {
|
| try {
|
| const finalPass = passphrase || "Fritree_MeyaMeya_EmptyPasswordFallback_2026_StrictSHA256";
|
| const rawPayload = JSON.stringify({
|
| ...capsuleData,
|
| protocolVersion: "5.0-EnterpriseCapsule",
|
| createdAt: new Date().toISOString(),
|
| nonce: generateSecureIdentifier(32)
|
| });
|
|
|
| const salt = generateSecureRandomBytes(16);
|
| const iv = generateSecureRandomBytes(12);
|
| const derivedKey = await derivePassphraseKey(finalPass, salt);
|
| if (!derivedKey) return null;
|
|
|
| const compressedBytes = await compressPayload(rawPayload);
|
| const ciphertextBuffer = await crypto.subtle.encrypt(
|
| { name: "AES-GCM", iv: iv, tagLength: 128 },
|
| derivedKey,
|
| compressedBytes
|
| );
|
|
|
| const serialized = {
|
| salt: uint8ArrayToBase64(salt),
|
| iv: uint8ArrayToBase64(iv),
|
| ct: uint8ArrayToBase64(new Uint8Array(ciphertextBuffer)),
|
| v: "V5-Capsule"
|
| };
|
|
|
| secureMemoryWipe(compressedBytes);
|
| return CAPSULE_HEADER + btoa(JSON.stringify(serialized));
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function decryptConditionalCapsulePayload(cipherText, passphrase = "") {
|
| if (!cipherText || typeof cipherText !== 'string' || !cipherText.startsWith(CAPSULE_HEADER)) {
|
| return null;
|
| }
|
| try {
|
| const finalPass = passphrase || "Fritree_MeyaMeya_EmptyPasswordFallback_2026_StrictSHA256";
|
| const rawB64 = cipherText.slice(CAPSULE_HEADER.length);
|
| const envelope = JSON.parse(atob(rawB64));
|
|
|
| const salt = base64ToUint8Array(envelope.salt);
|
| const iv = base64ToUint8Array(envelope.iv);
|
| const ciphertext = base64ToUint8Array(envelope.ct);
|
|
|
| const derivedKey = await derivePassphraseKey(finalPass, salt);
|
| if (!derivedKey) return null;
|
|
|
| const decryptedBuffer = await crypto.subtle.decrypt(
|
| { name: "AES-GCM", iv: iv, tagLength: 128 },
|
| derivedKey,
|
| ciphertext
|
| );
|
|
|
| const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer));
|
| secureMemoryWipe(decryptedBuffer);
|
| return JSON.parse(decompressedString);
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function auditStorageIntegritySeal(key) {
|
| try {
|
| const currentData = await global.FritreeCrypto.getStorage(key, null);
|
| if (currentData === null) return { valid: true, isClean: true };
|
|
|
| const stringified = typeof currentData === 'object' ? JSON.stringify(currentData) : String(currentData);
|
| const payloadString = `${key}:${stringified}:${INTEGRITY_SEAL_SALT}`;
|
| const expectedHash = await nativeHashSha256(payloadString);
|
|
|
| const savedHash = await global.FritreeStorage.get(`local_sis_sig_${key}`, null);
|
| if (!savedHash) {
|
| await global.FritreeStorage.set(`local_sis_sig_${key}`, expectedHash);
|
| return { valid: true, isClean: true, repaired: true };
|
| }
|
|
|
| return {
|
| valid: savedHash === expectedHash,
|
| expected: expectedHash,
|
| saved: savedHash
|
| };
|
| } catch (e) {
|
| silentTrap(e);
|
| return { valid: false, error: e.message };
|
| }
|
| }
|
|
|
| |
| |
|
|
| global.FritreeCrypto = {
|
| setLocal: function(key, value) {
|
| try {
|
| encryptAndSignPayload(value, key).then(encrypted => {
|
| if (encrypted) {
|
| localStorage.setItem(syncHashKey(key), encrypted);
|
| }
|
| });
|
| return true;
|
| } catch (e) {
|
| silentTrap(e);
|
| return false;
|
| }
|
| },
|
|
|
| getLocal: function(key) {
|
| try {
|
| const cipher = localStorage.getItem(syncHashKey(key));
|
| if (!cipher) return null;
|
| return verifyAndDecryptPayload(cipher, key);
|
| } catch (e) {
|
| silentTrap(e);
|
| return null;
|
| }
|
| },
|
|
|
| removeLocal: function(key) {
|
| try {
|
| localStorage.removeItem(syncHashKey(key));
|
| } catch (e) {
|
| silentTrap(e);
|
| }
|
| },
|
|
|
| setStorage: function(key, value) {
|
| return new Promise(async (resolve) => {
|
| try {
|
| const obfuscatedKey = await asyncHashKey(key);
|
| const encryptedValue = await encryptAndSignPayload(value, key);
|
| if (!encryptedValue) {
|
| resolve(false);
|
| return;
|
| }
|
|
|
| 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) {
|
| silentTrap(e);
|
| resolve(false);
|
| }
|
| });
|
| },
|
|
|
| getStorage: function(key, defaultValue = null) {
|
| return new Promise(async (resolve) => {
|
| try {
|
| 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) => {
|
| try {
|
| const cipher = result[obfuscatedKey];
|
| if (cipher === undefined || cipher === null) {
|
| resolve(defaultValue);
|
| return;
|
| }
|
| const decrypted = await verifyAndDecryptPayload(cipher, key);
|
| resolve(decrypted !== null ? decrypted : defaultValue);
|
| } catch (inn) {
|
| silentTrap(inn);
|
| resolve(defaultValue);
|
| }
|
| });
|
| } catch (e) {
|
| silentTrap(e);
|
| resolve(defaultValue);
|
| }
|
| });
|
| },
|
|
|
| getOrGenerateAccountId: async function() {
|
| try {
|
| 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;
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fallbackAccountId128Chars" + "a".repeat(101);
|
| }
|
| },
|
|
|
| regenerateAccountId: async function() {
|
| try {
|
| const newId = generateSecureIdentifier(128);
|
| await this.setStorage('fritree_account_id', newId);
|
| return newId;
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fallbackAccountId128Chars" + "b".repeat(101);
|
| }
|
| },
|
|
|
| encryptTokenPayload: async function(pointsObj, targetAccountId, password) {
|
| try {
|
| const finalPass = password || "";
|
| const payload = JSON.stringify({
|
| points: pointsObj,
|
| targetAccountId: targetAccountId,
|
| tokenId: "tk_v5_aead_" + Date.now().toString(36) + "_" + generateSecureIdentifier(16),
|
| timestamp: new Date().toISOString()
|
| });
|
|
|
| const salt = generateSecureRandomBytes(16);
|
| const iv = generateSecureRandomBytes(12);
|
| const derivedKey = await derivePassphraseKey(finalPass, salt);
|
| if (!derivedKey) return null;
|
|
|
| const compressedBytes = await compressPayload(payload);
|
| const ciphertextBuffer = await crypto.subtle.encrypt(
|
| { name: "AES-GCM", iv: iv, tagLength: 128 },
|
| derivedKey,
|
| compressedBytes
|
| );
|
|
|
| const serializedStructure = {
|
| salt: uint8ArrayToBase64(salt),
|
| iv: uint8ArrayToBase64(iv),
|
| ciphertext: uint8ArrayToBase64(new Uint8Array(ciphertextBuffer))
|
| };
|
|
|
| secureMemoryWipe(compressedBytes);
|
| return TOKEN_HEADER + btoa(JSON.stringify(serializedStructure));
|
| } catch (e) {
|
| silentTrap(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 = base64ToUint8Array(serializedStructure.salt);
|
| const iv = base64ToUint8Array(serializedStructure.iv);
|
| const ciphertext = base64ToUint8Array(serializedStructure.ciphertext);
|
|
|
| const derivedKey = await derivePassphraseKey(finalPass, salt);
|
| if (!derivedKey) return null;
|
|
|
| const decryptedBuffer = await crypto.subtle.decrypt(
|
| { name: "AES-GCM", iv: iv, tagLength: 128 },
|
| derivedKey,
|
| ciphertext
|
| );
|
|
|
| const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer));
|
| secureMemoryWipe(decryptedBuffer);
|
| return JSON.parse(decompressedString);
|
| } catch (e) {
|
| silentTrap(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);
|
| if (!derivedKey) return null;
|
|
|
| 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: uint8ArrayToBase64(salt),
|
| iv: uint8ArrayToBase64(iv),
|
| ciphertext: uint8ArrayToBase64(new Uint8Array(ciphertextBuffer))
|
| };
|
|
|
| const serialized = BACKUP_HEADER + btoa(JSON.stringify(payload));
|
| secureMemoryWipe(compressedBytes);
|
| return serialized;
|
| } catch (e) {
|
| silentTrap(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 = base64ToUint8Array(payload.salt);
|
| const iv = base64ToUint8Array(payload.iv);
|
| const ciphertext = base64ToUint8Array(payload.ciphertext);
|
|
|
| const derivedKey = await derivePassphraseKey(finalPass, salt);
|
| if (!derivedKey) return null;
|
|
|
| const decryptedBuffer = await crypto.subtle.decrypt(
|
| { name: "AES-GCM", iv: iv, tagLength: 128 },
|
| derivedKey,
|
| ciphertext
|
| );
|
|
|
| const decompressedString = await decompressPayload(new Uint8Array(decryptedBuffer));
|
| secureMemoryWipe(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 e;
|
| }
|
| silentTrap(e);
|
| return null;
|
| }
|
| },
|
|
|
| signReceipt: async function(prefix, dataLength, actionType, payloadString, integritySalt) {
|
| try {
|
| const rawText = `${prefix}:${dataLength}:${actionType}:${payloadString}:${integritySalt}`;
|
| return await nativeHashSha256(rawText + INTEGRITY_SEAL_SALT);
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fallback_signature_" + dataLength;
|
| }
|
| },
|
|
|
| encryptConditionalCapsulePayload,
|
| decryptConditionalCapsulePayload,
|
| auditStorageIntegritySeal,
|
| rotateKeys: rotateSymmetricKeys,
|
| validateData: DataValidationPipeline.validate,
|
| generateRandomBytes: generateSecureRandomBytes,
|
| generateUID: generateSecureIdentifier,
|
| shake256: function(msg, len = 64) {
|
| try {
|
| let hash = 0;
|
| const input = String(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);
|
| } catch (e) {
|
| silentTrap(e);
|
| return "fallback_shake_" + len;
|
| }
|
| },
|
| sha256: async function(msg) {
|
| return await nativeHashSha256(msg);
|
| },
|
| encryptAndSignPayload: encryptAndSignPayload,
|
| verifyAndDecryptPayload: verifyAndDecryptPayload,
|
| asyncHashKey: asyncHashKey,
|
| syncHashKey: syncHashKey
|
| };
|
|
|
| })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this); |