init
This commit is contained in:
+122
@@ -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
|
||||||
Generated
+1851
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export { useCryptoLocker } from "./useCryptoLocker";
|
||||||
|
export type { CryptoLockerStatus } from "./useCryptoLocker";
|
||||||
|
export type {
|
||||||
|
CryptoLockerModule,
|
||||||
|
CryptoLockerPluginOptions,
|
||||||
|
} from "./types";
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
Generated
+1855
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user