121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const root = path.resolve(import.meta.dir, "..");
|
|
const app = process.argv[2] ?? "MoanNote";
|
|
if (!/^[A-Za-z0-9_-]+$/.test(app))
|
|
throw new Error("App name must be a simple file name");
|
|
const unsignedApk = path.join(root, "outputs", `${app}_patched.apk`);
|
|
const signedApk = path.join(root, "outputs", `${app}_patched-signed.apk`);
|
|
const signingDir = path.join(root, "build", "signing");
|
|
const keyName = `${app.toLowerCase()}-dev`;
|
|
const keystore = path.join(signingDir, `${keyName}.jks`);
|
|
const credentials = path.join(signingDir, `${keyName}.env`);
|
|
|
|
function run(command: string, args: string[]): void {
|
|
const result = spawnSync(command, args, { cwd: root, stdio: "inherit" });
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0)
|
|
throw new Error(
|
|
`${path.basename(command)} failed with status ${result.status}`,
|
|
);
|
|
}
|
|
|
|
function readCredentials(): Record<string, string> {
|
|
return Object.fromEntries(
|
|
fs
|
|
.readFileSync(credentials, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => line.split("=", 2) as [string, string]),
|
|
);
|
|
}
|
|
|
|
function androidTool(name: "apksigner" | "zipalign"): string {
|
|
const sdk =
|
|
process.env.ANDROID_HOME ??
|
|
process.env.ANDROID_SDK_ROOT ??
|
|
"/var/home/hellings/android-sdk";
|
|
for (const version of ["36.0.0", "35.0.0", "34.0.0"]) {
|
|
const tool = path.join(sdk, "build-tools", version, name);
|
|
if (fs.existsSync(tool)) return tool;
|
|
}
|
|
throw new Error(`Android ${name} was not found under ${sdk}/build-tools`);
|
|
}
|
|
|
|
function ensureDevKey(): Record<string, string> {
|
|
fs.mkdirSync(signingDir, { recursive: true, mode: 0o700 });
|
|
if (!fs.existsSync(keystore)) {
|
|
const password = crypto.randomBytes(32).toString("base64url");
|
|
const values = {
|
|
storePassword: password,
|
|
keyPassword: password,
|
|
alias: keyName,
|
|
};
|
|
run("keytool", [
|
|
"-genkeypair",
|
|
"-keystore",
|
|
keystore,
|
|
"-storetype",
|
|
"PKCS12",
|
|
"-storepass",
|
|
password,
|
|
"-alias",
|
|
values.alias,
|
|
"-keypass",
|
|
password,
|
|
"-keyalg",
|
|
"RSA",
|
|
"-keysize",
|
|
"2048",
|
|
"-validity",
|
|
"3650",
|
|
"-dname",
|
|
`CN=${app} Development,O=rip.crit.ms,C=US`,
|
|
]);
|
|
fs.writeFileSync(
|
|
credentials,
|
|
Object.entries(values)
|
|
.map(([key, value]) => `${key}=${value}`)
|
|
.join("\n") + "\n",
|
|
{ mode: 0o600 },
|
|
);
|
|
console.log(`[ok] Created local ${app} development key at ${keystore}`);
|
|
}
|
|
if (!fs.existsSync(credentials))
|
|
throw new Error(
|
|
`Development key credentials are missing at ${credentials}`,
|
|
);
|
|
return readCredentials();
|
|
}
|
|
|
|
if (!fs.existsSync(unsignedApk))
|
|
throw new Error(`Patched APK is missing at ${unsignedApk}`);
|
|
const values = ensureDevKey();
|
|
const aligned = path.join(root, "build", `${app}_patched-aligned.apk`);
|
|
fs.mkdirSync(path.dirname(aligned), { recursive: true });
|
|
run(androidTool("zipalign"), ["-f", "-p", "4", unsignedApk, aligned]);
|
|
run(androidTool("apksigner"), [
|
|
"sign",
|
|
"--ks",
|
|
keystore,
|
|
"--ks-key-alias",
|
|
values.alias!,
|
|
"--ks-pass",
|
|
`pass:${values.storePassword!}`,
|
|
"--key-pass",
|
|
`pass:${values.keyPassword!}`,
|
|
"--out",
|
|
signedApk,
|
|
aligned,
|
|
]);
|
|
run(androidTool("apksigner"), [
|
|
"verify",
|
|
"--verbose",
|
|
"--print-certs",
|
|
signedApk,
|
|
]);
|
|
console.log(`[ok] Signed ${signedApk}`);
|