init
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user