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
+122
View File
@@ -0,0 +1,122 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# OS metadata
.DS_Store
Thumbs.db
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.swp
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
.env.test
.env*.local
+1
View File
@@ -0,0 +1 @@
#### Created by Claude Opus 4.6
+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",
},
},
},
},
});
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CryptoLocker — Приклад</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1855
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "crypto-locker-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@crypto-locker/core": "file:../core",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.5.2",
"typescript": "^5.8.0",
"vite": "^6.3.5"
}
}
+173
View File
@@ -0,0 +1,173 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
:root {
--font: "Inter", system-ui, -apple-system, sans-serif;
--bg: #0a0a0f;
--bg-card: rgba(18, 18, 26, 0.75);
--accent-1: #6366f1;
--accent-2: #8b5cf6;
--gradient: linear-gradient(135deg, var(--accent-1), var(--accent-2));
--text: #f0f0f5;
--text-muted: #6b7280;
--border: rgba(255, 255, 255, 0.06);
--error: #ef4444;
--success: #34d399;
--radius: 16px;
--radius-sm: 10px;
--ease: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100dvh;
-webkit-font-smoothing: antialiased;
}
#root {
min-height: 100dvh;
}
/* Layout */
.app {
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
position: relative;
}
.app::before {
content: "";
position: absolute;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(99, 102, 241, 0.12), transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
animation: glow 6s ease-in-out infinite;
}
@keyframes glow {
0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.15); }
}
.app__container {
width: 100%;
max-width: 440px;
position: relative;
z-index: 1;
}
/* CryptoLocker form overrides */
form {
display: flex;
flex-direction: column;
gap: 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 40px 32px;
box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(24px);
animation: enter 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes enter {
from { opacity: 0; transform: translateY(16px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
form input[type="password"] {
width: 100%;
padding: 14px 18px;
font-family: var(--font);
font-size: 0.95rem;
color: var(--text);
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
outline: none;
transition: background var(--ease), border-color var(--ease), box-shadow var(--ease);
}
form input[type="password"]::placeholder { color: var(--text-muted); }
form input[type="password"]:focus {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(99, 102, 241, 0.5);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
form p { color: var(--error); font-size: 0.85rem; text-align: center; }
form button {
width: 100%;
padding: 14px;
font-family: var(--font);
font-size: 0.95rem;
font-weight: 600;
color: #fff;
background: var(--gradient);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: transform var(--ease), box-shadow var(--ease);
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.25);
}
form button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.35);
}
form button:disabled { opacity: 0.5; cursor: not-allowed; }
/* Secret content */
.secret-content {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 40px 32px;
text-align: center;
box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(24px);
animation: enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.secret-content__badge {
display: inline-block;
padding: 5px 14px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--success);
background: rgba(52, 211, 153, 0.1);
border: 1px solid rgba(52, 211, 153, 0.15);
border-radius: 999px;
margin-bottom: 16px;
}
.secret-content__message {
font-size: 1.25rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
+55
View File
@@ -0,0 +1,55 @@
import React, { useState } from "react";
import { useCryptoLocker } from "@crypto-locker/core";
import "./App.css";
// The decrypted data will be a React component type
type SecretComponentType = React.ComponentType;
export default function App() {
const { unlock, status, decryptedData: SecretComponent, error } = useCryptoLocker<SecretComponentType>();
const [password, setPassword] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!password.trim()) return;
try {
await unlock(() => import("./secret-content.secret"), password);
} catch {
// The error is already caught and managed by useCryptoLocker's state.
}
};
return (
<div className="app">
<div className="app__container">
{status === "success" && SecretComponent ? (
<SecretComponent />
) : (
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "1rem", maxWidth: "300px", margin: "0 auto" }}>
<h2>Enter Password</h2>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password…"
disabled={status === "loading"}
style={{ padding: "0.5rem", fontSize: "1rem" }}
/>
{error && <p style={{ color: "red", margin: 0 }}>{error}</p>}
<button
type="submit"
disabled={status === "loading" || !password.trim()}
style={{ padding: "0.5rem 1rem", fontSize: "1rem", cursor: "pointer" }}
>
{status === "loading" ? "Loading…" : "Unlock"}
</button>
</form>
)}
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
/**
* Secret content — plain React Component.
*
* The Vite plugin 'cryptoLockerPlugin' automatically encrypts
* this module's source code during build/dev. In the build artifacts,
* there will be only an AES-encrypted string.
*
* Test password (in .env): my-super-secret-password
*/
export default function SecretComponent() {
return (
<div className="secret-content" style={{ marginTop: "1rem" }}>
<div className="secret-content__badge" style={{ color: "green", fontWeight: "bold" }}>
Decrypted
</div>
<p className="secret-content__message" style={{ fontSize: "1.2rem", marginTop: "0.5rem" }}>
Success from a React Component!
</p>
<ul style={{ marginTop: "1rem", textAlign: "left", display: "inline-block", color: "#555" }}>
<li>🔐 Encrypted at build time</li>
<li>🛡 Decrypted and evaluated at runtime</li>
<li>🚀 Fully functional React component</li>
</ul>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
]
},
"include": [
"src"
]
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import { cryptoLockerPlugin } from "@crypto-locker/core/plugin";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
react({
exclude: /\.secret\.[jt]sx?$/,
}),
cryptoLockerPlugin({
password: env.LOCKER_PASSWORD || "secret123",
}),
],
};
});