OpenMiInk/scripts/sign-moannotes.ts
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

70 lines
3.1 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}`);