Initial commit
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
import type { StringScrambler } from "../src/utils/stringtool";
|
||||
|
||||
/**
|
||||
* Sentinel string in `src/utils/runtimeDecoder.ts` that the build
|
||||
* pipelines rewrite with the freshly-generated passphrase for the
|
||||
* current build.
|
||||
*
|
||||
* Keep this in sync with the literal in `runtimeDecoder.ts`.
|
||||
*/
|
||||
export const RUNTIME_PASSPHRASE_PLACEHOLDER =
|
||||
"__SCRAMBLE_BUILD_PASSPHRASE__";
|
||||
|
||||
/**
|
||||
* Sentinel string for the per-build salt injected into the runtime
|
||||
* decoder. Same mechanism as the passphrase placeholder.
|
||||
*/
|
||||
export const RUNTIME_SALT_PLACEHOLDER = "__SCRAMBLE_BUILD_SALT__";
|
||||
|
||||
export const RUNTIME_FN_NAME_PLACEHOLDER = "__SCRAMBLE_FN_NAME__";
|
||||
|
||||
export const RUNTIME_DECODER_PATH = "src/utils/runtimeDecoder.ts";
|
||||
|
||||
/**
|
||||
* Regex used to find `scramble(...)` calls in source code.
|
||||
*
|
||||
* Accepts either a double-quoted or backtick-quoted single string
|
||||
* literal as the only argument. Single-quoted strings, concatenations,
|
||||
* and template interpolations are intentionally not supported — those
|
||||
* would not survive the textual transform safely.
|
||||
*/
|
||||
export const SCRAMBLE_CALL_REGEX =
|
||||
/scramble\(\s*(`[\s\S]*?`|"[\s\S]*?")\s*,?\s*\)/g;
|
||||
|
||||
/**
|
||||
* Regex used to strip out `declare function scramble(...)` lines from
|
||||
* the transformed source. The runtime has no `scramble` symbol — only
|
||||
* `beautify` — so the declaration is dead weight at runtime.
|
||||
*/
|
||||
export const SCRAMBLE_DECLARE_REGEX =
|
||||
/declare\s+function\s+scramble[^;]*;\s*\n?/g;
|
||||
|
||||
/**
|
||||
* Generates a fresh random passphrase to be used for this build.
|
||||
*
|
||||
* The passphrase is 64 hex characters (32 random bytes). It is meant to
|
||||
* be ephemeral: it is generated once per build, used to encode every
|
||||
* `scramble(...)` call site, and then baked into the runtime decoder so
|
||||
* that decoding works at runtime without any environment variables.
|
||||
*/
|
||||
export function generateBuildPassphrase(): string {
|
||||
return randomBytes(32).toString("hex");
|
||||
}
|
||||
|
||||
export function generateBuildSalt(): string {
|
||||
return randomBytes(16).toString("hex");
|
||||
}
|
||||
|
||||
export function generateFunctionName(): string {
|
||||
return "f" + randomBytes(4).toString("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a single source file's text by replacing every
|
||||
* `scramble("...")` / `` scramble(`...`) `` call with a
|
||||
* `beautify("<base64>")` call encoded with the supplied
|
||||
* scrambler, and stripping out the matching `declare function scramble`
|
||||
* statements.
|
||||
*
|
||||
* The transform is purely textual; it makes no attempt to parse the
|
||||
* source. The constraints documented on `SCRAMBLE_CALL_REGEX` apply.
|
||||
*
|
||||
* @param code The original source code.
|
||||
* @param scrambler The `StringScrambler` to use for encoding.
|
||||
* @param logPrefix Optional log prefix for build output (e.g. "[BUILD]").
|
||||
* @param sourceLabel Optional label (filename) included in log output.
|
||||
*/
|
||||
export function transformSource(
|
||||
code: string,
|
||||
scrambler: StringScrambler,
|
||||
fnName: string,
|
||||
logPrefix = "[SCRAMBLE]",
|
||||
sourceLabel?: string,
|
||||
): { code: string; replacements: number } {
|
||||
let replacements = 0;
|
||||
|
||||
const transformed = code.replace(
|
||||
SCRAMBLE_CALL_REGEX,
|
||||
(_match, str: string) => {
|
||||
const inner = str.slice(1, -1);
|
||||
const encoded = scrambler.encode(inner);
|
||||
replacements++;
|
||||
const where = sourceLabel ? ` in ${sourceLabel}` : "";
|
||||
console.log(
|
||||
`${logPrefix} scramble(${str.slice(0, 32)}...) -> ${fnName}("${encoded.slice(0, 16)}...")${where}`,
|
||||
);
|
||||
return `${fnName}(${JSON.stringify(encoded)})`;
|
||||
},
|
||||
);
|
||||
|
||||
const stripped = transformed.replace(SCRAMBLE_DECLARE_REGEX, "");
|
||||
|
||||
return { code: stripped, replacements };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the runtime decoder source, replaces the build-time placeholder
|
||||
* passphrase with the supplied real passphrase, and returns the new
|
||||
* contents. The original file on disk is NOT modified — callers are
|
||||
* expected to write the rewritten contents to a temp/output location.
|
||||
*
|
||||
* Throws if the placeholder cannot be found, which would otherwise
|
||||
* silently produce a bundle that decodes to garbage at runtime.
|
||||
*/
|
||||
export async function rewriteRuntimeDecoder(
|
||||
decoderPath: string,
|
||||
passphrase: string,
|
||||
salt: string,
|
||||
fnName: string,
|
||||
): Promise<string> {
|
||||
const original = await fs.readFile(decoderPath, "utf-8");
|
||||
|
||||
let code = original;
|
||||
|
||||
for (const [placeholder, value] of [
|
||||
[RUNTIME_PASSPHRASE_PLACEHOLDER, passphrase],
|
||||
[RUNTIME_SALT_PLACEHOLDER, salt],
|
||||
[RUNTIME_FN_NAME_PLACEHOLDER, fnName],
|
||||
] as const) {
|
||||
if (!code.includes(placeholder)) {
|
||||
throw new Error(
|
||||
`[SCRAMBLE] Could not find placeholder "${placeholder}" in ${decoderPath}.`,
|
||||
);
|
||||
}
|
||||
const quoted = `"${placeholder}"`;
|
||||
code = code.split(quoted).join(JSON.stringify(value));
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
Reference in New Issue
Block a user