This commit is contained in:
Vitalii Litvinchuk
2026-06-10 15:09:45 +03:00
commit dc8c379ecf
20 changed files with 4457 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import { useState, useCallback } from "react";
import CryptoJS from "crypto-js";
import React from "react";
import type { CryptoLockerModule } from "./types";
export type CryptoLockerStatus = "idle" | "loading" | "success" | "error";
/**
* Hook that provides logic for AES decryption of dynamic modules.
* UI, password handling, and validation are fully delegated to the developer.
*/
export function useCryptoLocker<T = unknown>() {
const [status, setStatus] = useState<CryptoLockerStatus>("idle");
const [decryptedData, setDecryptedData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
/**
* Decrypts the dynamically loaded module and evaluates its code.
*
* @param fetchData A function that calls dynamic `import()` for the secret module.
* @param password The AES password.
*/
const unlock = useCallback(
async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchData: () => Promise<any>,
password: string
): Promise<T> => {
setStatus("loading");
setError(null);
try {
const mod = (await fetchData()) as CryptoLockerModule;
const ciphertext = mod.encryptedData;
if (typeof ciphertext !== "string") {
throw new Error("Module does not export 'encryptedData'.");
}
const bytes = CryptoJS.AES.decrypt(ciphertext, password);
const plaintextCode = bytes.toString(CryptoJS.enc.Utf8);
if (!plaintextCode) {
throw new Error("Invalid password or empty decryption result.");
}
// Evaluate the decrypted CommonJS code
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exportsObj: any = {};
const moduleObj = { exports: exportsObj };
// Provide React dependency to the evaluated module
const requireFn = (id: string) => {
if (id === "react") return React;
throw new Error(`Cannot require '${id}' in encrypted module.`);
};
const fn = new Function(
"require",
"exports",
"module",
"React",
plaintextCode
);
fn(requireFn, exportsObj, moduleObj, React);
const parsed = (moduleObj.exports.default ?? moduleObj.exports) as T;
setDecryptedData(() => parsed);
setStatus("success");
return parsed;
} catch (err: unknown) {
console.error("CryptoLocker decryption failed:", err);
const errMsg = err instanceof Error ? err.message : "Decryption failed";
setError(errMsg);
setStatus("error");
setDecryptedData(null);
throw err;
}
},
[]
);
/**
* Resets the hook state to idle, clearing any decrypted data and errors.
*/
const reset = useCallback(() => {
setStatus("idle");
setDecryptedData(null);
setError(null);
}, []);
return { unlock, reset, status, decryptedData, error };
}