'init'
This commit is contained in:
115
app/crypto/crypto.ts
Normal file
115
app/crypto/crypto.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { sha256, md5 } from "node-forge";
|
||||
import { generateRandomKey } from "../utils/utils";
|
||||
import * as secp256k1 from '@noble/secp256k1';
|
||||
|
||||
const worker = new Worker(new URL('./crypto.worker.ts', import.meta.url), { type: 'module' });
|
||||
|
||||
export const encodeWithPassword = async (password : string, data : any) : Promise<any> => {
|
||||
let task = generateRandomKey(16);
|
||||
return new Promise((resolve, _) => {
|
||||
worker.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data.action === 'encodeWithPasswordResult' && event.data.task === task) {
|
||||
resolve(event.data.result);
|
||||
}
|
||||
});
|
||||
worker.postMessage({ action: 'encodeWithPassword', data: { password, payload: data, task } });
|
||||
});
|
||||
}
|
||||
|
||||
export const decodeWithPassword = (password : string, data : any) : Promise<any> => {
|
||||
let task = generateRandomKey(16);
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data.action === 'decodeWithPasswordResult' && event.data.task === task) {
|
||||
if(event.data.result === null){
|
||||
reject("Decryption failed");
|
||||
return;
|
||||
}
|
||||
resolve(event.data.result);
|
||||
}
|
||||
});
|
||||
worker.postMessage({ action: 'decodeWithPassword', data: { password, payload: data, task } });
|
||||
});
|
||||
}
|
||||
|
||||
export const generateKeyPairFromSeed = async (seed : string) => {
|
||||
//generate key pair using secp256k1 includes privatekey from seed
|
||||
const privateKey = sha256.create().update(seed).digest().toHex().toString();
|
||||
const publicKey = secp256k1.getPublicKey(Buffer.from(privateKey, "hex"), true);
|
||||
return {
|
||||
privateKey: privateKey,
|
||||
publicKey: Buffer.from(publicKey).toString('hex'),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export const encrypt = async (data : string, publicKey : string) : Promise<any> => {
|
||||
let task = generateRandomKey(16);
|
||||
return new Promise((resolve, _) => {
|
||||
worker.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data.action === 'encryptResult' && event.data.task === task) {
|
||||
resolve(event.data.result);
|
||||
}
|
||||
});
|
||||
worker.postMessage({ action: 'encrypt', data: { publicKey, payload: data, task } });
|
||||
});
|
||||
}
|
||||
|
||||
export const decrypt = async (data : string, privateKey : string) : Promise<any> => {
|
||||
let task = generateRandomKey(16);
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data.action === 'decryptResult' && event.data.task === task) {
|
||||
if(event.data.result === null){
|
||||
reject("Decryption failed");
|
||||
return;
|
||||
}
|
||||
resolve(event.data.result);
|
||||
}
|
||||
});
|
||||
worker.postMessage({ action: 'decrypt', data: { privateKey, payload: data, task } });
|
||||
});
|
||||
}
|
||||
|
||||
export const chacha20Encrypt = async (data : string) => {
|
||||
let task = generateRandomKey(16);
|
||||
return new Promise((resolve, _) => {
|
||||
worker.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data.action === 'chacha20EncryptResult' && event.data.task === task) {
|
||||
resolve(event.data.result);
|
||||
}
|
||||
});
|
||||
worker.postMessage({ action: 'chacha20Encrypt', data: { payload: data, task } });
|
||||
});
|
||||
}
|
||||
|
||||
export const chacha20Decrypt = async (ciphertext : string, nonce : string, key : string) => {
|
||||
let task = generateRandomKey(16);
|
||||
return new Promise((resolve, _) => {
|
||||
worker.addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data.action === 'chacha20DecryptResult' && event.data.task === task) {
|
||||
resolve(event.data.result);
|
||||
}
|
||||
});
|
||||
worker.postMessage({ action: 'chacha20Decrypt', data: { ciphertext, nonce, key, task } });
|
||||
});
|
||||
}
|
||||
|
||||
export const generateMd5 = async (data : string) => {
|
||||
const hash = md5.create();
|
||||
hash.update(data);
|
||||
return hash.digest().toHex();
|
||||
}
|
||||
|
||||
export const generateHashFromPrivateKey = async (privateKey : string) => {
|
||||
return sha256.create().update(privateKey + "rosetta").digest().toHex().toString();
|
||||
}
|
||||
|
||||
export const isEncodedWithPassword = (data : string) => {
|
||||
try{
|
||||
atob(data).split(":");
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
316
app/crypto/crypto.worker.ts
Normal file
316
app/crypto/crypto.worker.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user