OpenMiInk/scripts/sign-moannotes.ts
Hellings 1e953b8132 ok
2026-10-16 13:05:27 -04:00

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