import crypto from 'crypto-js'; import pako from 'pako'; import { randomBytes } from "@noble/ciphers/webcrypto"; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import * as secp256k1 from '@noble/secp256k1'; self.onmessage = async (event: MessageEvent) => { const { action, data } = event.data; switch (action) { case 'encodeWithPassword': { const { password, payload, task } = data; const result = await encodeWithPassword(password, payload); self.postMessage({ action: 'encodeWithPasswordResult', result, task }); break; } case 'chacha20Encrypt': { const { payload, task } = data; const result = await chacha20Encrypt(payload); self.postMessage({ action: 'chacha20EncryptResult', result, task }); break; } case 'chacha20Decrypt': { const { ciphertext, nonce, key, task } = data; const result = await chacha20Decrypt(ciphertext, nonce, key); self.postMessage({ action: 'chacha20DecryptResult', result, task }); break; } case 'decodeWithPassword': { const { password, payload, task } = data; try{ const result = await decodeWithPassword(password, payload); self.postMessage({ action: 'decodeWithPasswordResult', result, task }); return; }catch(e){ const result = null; self.postMessage({ action: 'decodeWithPasswordResult', result, task }); } break; } case 'decrypt': { const { payload: encryptedData, privateKey, task } = data; const result = await decrypt(encryptedData, privateKey); self.postMessage({ action: 'decryptResult', result, task }); break; } case 'encrypt': { const { payload: plainData, publicKey, task } = data; const result = await encrypt(plainData, publicKey); self.postMessage({ action: 'encryptResult', result, task }); break; } default: console.error(`Unknown action: ${action}`); } }; export const encrypt = async (data: string, publicKey: string) => { // Generate ephemeral key pair const ephemeralPrivateKey = secp256k1.utils.randomSecretKey() // Parse recipient's public key const recipientPublicKey = Buffer.from(publicKey, 'hex'); // Compute shared secret using ECDH const sharedPoint = secp256k1.getSharedSecret(ephemeralPrivateKey, recipientPublicKey, false); const sharedKey = Buffer.from(sharedPoint.slice(1, 33)).toString('hex'); // Use x-coordinate // Encrypt data const iv = crypto.lib.WordArray.random(16); const keyBytes = crypto.enc.Hex.parse(sharedKey); const encrypted = crypto.AES.encrypt(data, keyBytes, { iv }); return btoa(iv.toString(crypto.enc.Hex) + ':' + encrypted.ciphertext.toString(crypto.enc.Hex) + ':' + Buffer.from(ephemeralPrivateKey).toString('hex')); } export const decrypt = async (data: string, privateKey: string) => { const [ivHex, encryptedHex, ephemeralPrivateKeyHex] = atob(data).split(":"); // Parse keys const ephemeralPrivateKey = Buffer.from(ephemeralPrivateKeyHex, 'hex'); const privateKeyBytes = Buffer.from(privateKey, 'hex'); // Compute ephemeral public key const ephemeralPublicKey = secp256k1.getPublicKey(ephemeralPrivateKey, false); // Compute shared secret using ECDH const sharedPoint = secp256k1.getSharedSecret(privateKeyBytes, ephemeralPublicKey, false); const sharedKey = Buffer.from(sharedPoint.slice(1, 33)).toString('hex'); // Use x-coordinate // Decrypt data const iv = crypto.enc.Hex.parse(ivHex); const keyBytes = crypto.enc.Hex.parse(sharedKey); const decrypted = crypto.AES.decrypt(crypto.lib.CipherParams.create({ ciphertext: crypto.enc.Hex.parse(encryptedHex) }), keyBytes, { iv }); let decryptedText = ""; try { decryptedText = decrypted.toString(crypto.enc.Utf8); } catch(e) { console.info("pass: ", privateKey); console.info("data: ", data); console.error("Decryption error: ", e); } if (!decryptedText) { console.info("pass: ", privateKey); console.info("data: ", data); throw new Error("Decryption failed or resulted in an empty string."); } return decryptedText; } export const chacha20Encrypt = async (data : string) => { const key = randomBytes(32); const nonce = randomBytes(24); const cipher = xchacha20poly1305(key, nonce); const ciphertext = cipher.encrypt(Buffer.from(data)); return { ciphertext: Buffer.from(ciphertext).toString("hex"), nonce: Buffer.from(nonce).toString("hex"), key: Buffer.from(key).toString("hex"), } } export const chacha20Decrypt = async (ciphertext : string, nonce : string, key : string) => { const cipher = xchacha20poly1305(Buffer.from(key, "hex"), Buffer.from(nonce, "hex")); const decrypted = cipher.decrypt(Buffer.from(ciphertext, "hex")); return Buffer.from(decrypted).toString("utf-8"); } // Utility to check if data is old format (base64 + ":") function isOldFormat(data: string): boolean { try { const decoded = atob(data); return decoded.includes(':'); } catch { return false; } } // Helper: chunk Uint8Array function chunkArrayBuffer(buffer: Uint8Array, chunkSize: number): Uint8Array[] { const chunks: Uint8Array[] = []; for (let i = 0; i < buffer.length; i += chunkSize) { chunks.push(buffer.subarray(i, Math.min(i + chunkSize, buffer.length))); } return chunks; } // Helper: join base64 chunks with a marker function joinChunksBase64(chunks: string[]): string { // Use "::" as a separator, and prefix with "CHNK:" to indicate chunked return 'CHNK:' + chunks.join('::'); } // Helper: split chunked base64 string function splitChunksBase64(data: string): string[] { // Remove "CHNK:" prefix and split by "::" return data.slice(5).split('::'); } // New: compress, encrypt, base64 encode (not hex), with chunking for large payloads export const encodeWithPassword = async (password: string, payload: any) => { // Convert payload to string and compress const input = typeof payload === 'string' ? payload : JSON.stringify(payload); const compressed = pako.deflate(input); // If compressed > 10MB, chunk it const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB if (compressed.length > CHUNK_SIZE) { const chunks = chunkArrayBuffer(compressed, CHUNK_SIZE); const encryptedChunks: string[] = []; for (const chunk of chunks) { const key = crypto.PBKDF2(password, 'rosetta', { keySize: 256 / 32, iterations: 1000 }); const iv = crypto.lib.WordArray.random(16); const wordArray = crypto.lib.WordArray.create(chunk as any); const encrypted = crypto.AES.encrypt(wordArray, key, { iv }); // iv:ciphertext base64 const ivBase64 = crypto.enc.Base64.stringify(iv); const ctBase64 = crypto.enc.Base64.stringify(encrypted.ciphertext); encryptedChunks.push(ivBase64 + ':' + ctBase64); } return joinChunksBase64(encryptedChunks); } else { // Single chunk (as before) const key = crypto.PBKDF2(password, 'rosetta', { keySize: 256 / 32, iterations: 1000 }); const iv = crypto.lib.WordArray.random(16); const wordArray = crypto.lib.WordArray.create(compressed as any); const encrypted = crypto.AES.encrypt(wordArray, key, { iv }); const ivBase64 = crypto.enc.Base64.stringify(iv); const ctBase64 = crypto.enc.Base64.stringify(encrypted.ciphertext); return ivBase64 + ':' + ctBase64; } }; export const decodeWithPassword = async (password: string, data: any) => { // Handle old format (base64-encoded "iv:ciphertext") if (isOldFormat(data)) { const [ivHex, encryptedHex] = atob(data).split(':'); const iv = crypto.enc.Hex.parse(ivHex); const key = crypto.PBKDF2(password, crypto.enc.Utf8.parse('rosetta'), { keySize: 256 / 32, iterations: 1000 }); const decrypted = crypto.AES.decrypt( crypto.lib.CipherParams.create({ ciphertext: crypto.enc.Hex.parse(encryptedHex) }), key, { iv } ); const decryptedUtf8 = decrypted.toString(crypto.enc.Utf8); if (!decryptedUtf8) throw new Error('Decryption failed or resulted in an empty string.'); return decryptedUtf8; } // Check for chunked format if (typeof data === 'string' && data.startsWith('CHNK:')) { const chunkBase64s = splitChunksBase64(data); const decompressedParts: Uint8Array[] = []; for (const chunkBase64 of chunkBase64s) { const [ivBase64, ctBase64] = chunkBase64.split(':'); if (!ivBase64 || !ctBase64) throw new Error('Invalid encrypted chunk format.'); const iv = crypto.enc.Base64.parse(ivBase64); const key = crypto.PBKDF2(password, 'rosetta', { keySize: 256 / 32, iterations: 1000 }); const decrypted = crypto.AES.decrypt( crypto.lib.CipherParams.create({ ciphertext: crypto.enc.Base64.parse(ctBase64) }), key, { iv } ); // Convert decrypted to Uint8Array const decryptedWords = decrypted.words; const decryptedSigBytes = decrypted.sigBytes; const bytes = new Uint8Array(decryptedSigBytes); for (let i = 0; i < decryptedSigBytes; ++i) { bytes[i] = (decryptedWords[(i / 4) | 0] >> (24 - 8 * (i % 4))) & 0xff; } decompressedParts.push(bytes); } // Concatenate all parts const totalLength = decompressedParts.reduce((sum, arr) => sum + arr.length, 0); const allBytes = new Uint8Array(totalLength); let offset = 0; for (const arr of decompressedParts) { allBytes.set(arr, offset); offset += arr.length; } let decompressed: string; try { decompressed = new TextDecoder().decode(pako.inflate(allBytes)); } catch { throw new Error('Failed to decompress decrypted data.'); } if (!decompressed) throw new Error('Decryption failed or resulted in an empty string.'); return decompressed; } // New format: base64 "iv:ciphertext" const [ivBase64, ctBase64] = data.split(':'); if (!ivBase64 || !ctBase64) throw new Error('Invalid encrypted data format.'); const iv = crypto.enc.Base64.parse(ivBase64); const key = crypto.PBKDF2(password, 'rosetta', { keySize: 256 / 32, iterations: 1000 }); const decrypted = crypto.AES.decrypt( crypto.lib.CipherParams.create({ ciphertext: crypto.enc.Base64.parse(ctBase64) }), key, { iv } ); // Decompress const decryptedWords = decrypted.words; const decryptedSigBytes = decrypted.sigBytes; const decryptedBytes = new Uint8Array(decryptedSigBytes); for (let i = 0; i < decryptedSigBytes; ++i) { decryptedBytes[i] = (decryptedWords[(i / 4) | 0] >> (24 - 8 * (i % 4))) & 0xff; } let decompressed: string; try { decompressed = new TextDecoder().decode(pako.inflate(decryptedBytes)); } catch { throw new Error('Failed to decompress decrypted data.'); } if (!decompressed) { throw new Error('Decryption failed or resulted in an empty string.'); } return decompressed; };