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
+1851
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
{
"author": "CryptoLocker",
"license": "MIT",
"description": "AES encryption logic and Vite plugin for React components",
"keywords": [
"react",
"vite",
"encryption",
"crypto",
"security"
],
"name": "@crypto-locker/core",
"version": "1.0.0",
"type": "module",
"main": "./dist/crypto-locker.umd.cjs",
"module": "./dist/crypto-locker.js",
"exports": {
".": {
"import": "./dist/crypto-locker.js",
"require": "./dist/crypto-locker.umd.cjs"
},
"./plugin": "./src/plugin.ts"
},
"files": [
"dist",
"src"
],
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"dependencies": {
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.5.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript": "^5.8.0",
"vite": "^6.3.5"
},
"scripts": {
"build": "vite build"
}
}
+6
View File
@@ -0,0 +1,6 @@
export { useCryptoLocker } from "./useCryptoLocker";
export type { CryptoLockerStatus } from "./useCryptoLocker";
export type {
CryptoLockerModule,
CryptoLockerPluginOptions,
} from "./types";
+69
View File
@@ -0,0 +1,69 @@
import type { Plugin } from "vite";
import type { CryptoLockerPluginOptions } from "./types";
const DEFAULT_INCLUDE = /\.secret\.[jt]sx?$/;
/**
* Vite plugin for automatic AES encryption of modules during build/dev.
*
* Files matching the `include` pattern (default `*.secret.ts`),
* are transformed: their source code is compiled to CommonJS,
* encrypted via AES with the specified password, and replaced with:
*
* export const encryptedData = "<ciphertext>";
*
* The password never ends up in the build artifacts.
*/
export function cryptoLockerPlugin(
options: CryptoLockerPluginOptions,
): Plugin {
const { password } = options;
const pattern =
options.include != null
? typeof options.include === "string"
? new RegExp(options.include)
: options.include
: DEFAULT_INCLUDE;
if (!password) {
throw new Error(
"[crypto-locker] password is required. " +
"Set LOCKER_PASSWORD env variable or pass it in plugin options.",
);
}
return {
name: "vite-plugin-crypto-locker",
enforce: "pre",
async transform(code: string, id: string) {
// Ignore files that do not match the pattern
if (!pattern.test(id)) return null;
// Dynamic import of dependencies (available via Vite/Node)
const { transform: esbuildTransform } = await import("esbuild");
const CryptoJS = (await import("crypto-js")).default;
// 1. Compile TS/TSX → CJS JS via esbuild
// Use "transform" for JSX to avoid imports to "react/jsx-runtime"
const { code: jsCode } = await esbuildTransform(code, {
loader: id.match(/\.tsx$/) ? "tsx" : "ts",
format: "cjs",
target: "es2020",
jsx: "transform",
jsxFactory: "React.createElement",
jsxFragment: "React.Fragment"
});
// 2. Encrypt the compiled JavaScript code
const encrypted = CryptoJS.AES.encrypt(jsCode, password).toString();
// 3. Return the transformed module
return {
code: `export const encryptedData = ${JSON.stringify(encrypted)};\n`,
map: null,
};
},
};
}
+19
View File
@@ -0,0 +1,19 @@
import type { ReactNode } from "react";
/** Module transformed by the Vite plugin (contains the encrypted string). */
export interface CryptoLockerModule {
encryptedData: string;
}
/** Vite plugin options. */
export interface CryptoLockerPluginOptions {
/** Password for AES encryption during build. */
password: string;
/**
* File pattern to encrypt.
* @default /\.secret\.[jt]sx?$/
*/
include?: string | RegExp;
}
+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 };
}
+7
View File
@@ -0,0 +1,7 @@
const CryptoJS = require("crypto-js");
const ciphertext = "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";
const password = process.env.LOCKER_PASSWORD || "secret123";
const bytes = CryptoJS.AES.decrypt(ciphertext, password);
const plaintextCode = bytes.toString(CryptoJS.enc.Utf8);
console.log(plaintextCode ? "Decrypted!" : "Failed to decrypt!");
if (plaintextCode) console.log(plaintextCode.slice(0, 100));
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"outDir": "./dist",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
]
},
"include": [
"src"
]
}
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
export default defineConfig({
plugins: [react({ jsxRuntime: "automatic" })],
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "CryptoLocker",
fileName: "crypto-locker",
},
rollupOptions: {
external: ["react", "react-dom", "react/jsx-runtime", "crypto-js"],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
"react/jsx-runtime": "jsxRuntime",
"crypto-js": "CryptoJS",
},
},
},
},
});