This commit is contained in:
rosetta
2026-01-30 05:01:05 +02:00
commit 83f38dc63f
327 changed files with 18725 additions and 0 deletions

316
app/crypto/crypto.worker.ts Normal file
View File

@@ -0,0 +1,316 @@
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;
};