flotilla/scripts/release/steps/fdroid-sign.mjs

93 lines
3.3 KiB
JavaScript

import {existsSync, readFileSync, readdirSync} from "node:fs"
import {mkdtemp, readFile, rm, writeFile} from "node:fs/promises"
import {tmpdir} from "node:os"
import {join} from "node:path"
import {HOUR, MINUTE, ago, ms, now, sleep} from "@welshman/lib"
import {keystoreEnv} from "../lib/android.mjs"
import {missingEnv, root} from "../lib/context.mjs"
import {fdroidPackage, packageToken, signedApk, unsignedApk} from "../lib/fdroid.mjs"
import {output, run} from "../lib/shell.mjs"
const sdk =
process.env.ANDROID_HOME ??
readFileSync(join(root, "android/local.properties"), "utf-8").match(/^sdk\.dir=(.+)$/m)?.[1]
const apksigner = () => {
const tools = join(sdk, "build-tools")
const [latest] = readdirSync(tools).sort((a, b) => b.localeCompare(a, "en", {numeric: true}))
return join(tools, latest, "apksigner")
}
export default {
name: "fdroid-sign",
title: "Sign the release workflow's F-Droid build with the distribution key",
missing: () => [
...missingEnv("ANDROID_KEYSTORE_PATH", "ANDROID_KEYSTORE_PASSWORD", "ANDROID_KEYSTORE_ALIAS"),
...(packageToken ? [] : ["GITEA_TOKEN"]),
...(sdk && existsSync(join(sdk, "build-tools")) ? [] : ["the Android SDK's build-tools"]),
],
setup: [
"Signs with the same ANDROID_KEYSTORE_* key as the apk step. GITEA_TOKEN needs the",
"write:package scope. apksigner comes from the Android SDK in ANDROID_HOME, or the sdk.dir",
"Android Studio writes to android/local.properties.",
],
run: async () => {
const api = fdroidPackage(packageToken)
const started = now()
let unsigned = await api.download(unsignedApk)
while (!unsigned) {
if (started < ago(2 * HOUR)) {
throw new Error(
`The release workflow hasn't uploaded ${api.url(unsignedApk)} after two hours`,
)
}
console.log(`Waiting for the release workflow to upload ${unsignedApk}`)
await sleep(ms(MINUTE))
unsigned = await api.download(unsignedApk)
}
const work = await mkdtemp(join(tmpdir(), "flotilla-fdroid-sign-"))
try {
const signing = keystoreEnv("ANDROID")
await writeFile(join(work, unsignedApk), unsigned)
// F-Droid copies this signature onto its own build, so nothing but the signature may change
await run(
apksigner(),
[
"sign",
"--ks",
signing.ANDROID_KEYSTORE_PATH,
"--ks-key-alias",
signing.ANDROID_KEYSTORE_ALIAS,
"--ks-pass",
"env:ANDROID_KEYSTORE_PASSWORD",
"--key-pass",
"env:ANDROID_KEYSTORE_ALIAS_PASSWORD",
"--alignment-preserved",
"--out",
join(work, signedApk),
join(work, unsignedApk),
],
{env: {...process.env, ...signing}},
)
const recipe = readFileSync(join(root, "fdroid/metadata/social.flotilla.fdroid.yml"), "utf-8")
const allowed = recipe.match(/^AllowedAPKSigningKeys: (\w+)$/m)[1]
const certificates = output(apksigner(), ["verify", "--print-certs", join(work, signedApk)])
if (!certificates.includes(`certificate SHA-256 digest: ${allowed}`)) {
throw new Error(`${signedApk} isn't signed with the key the recipe allows, ${allowed}`)
}
console.log(await api.upload(signedApk, await readFile(join(work, signedApk))))
} finally {
await rm(work, {recursive: true, force: true})
}
},
}