78 lines
4.2 KiB
JavaScript
78 lines
4.2 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { createReadStream } from 'node:fs'
|
|
import { lstat } from 'node:fs/promises'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { parseArgs } from 'node:util'
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
export function validateAndroidRelease({ signature, badging, certificate, version, versionCode }) {
|
|
const expected = certificate.trim().replaceAll(':', '').toLowerCase()
|
|
if (!/^[a-f0-9]{64}$/.test(expected)) throw new Error('certificate-pin-invalid')
|
|
if (!/^\d+\.\d+\.\d+(?:-beta\.\d+)?$/.test(version)) throw new Error('release-version-invalid')
|
|
if (!Number.isSafeInteger(versionCode) || versionCode <= 0) throw new Error('version-code-invalid')
|
|
const signers = [...signature.matchAll(/^Signer #\d+ certificate SHA-256 digest: ([a-fA-F0-9]{64})\s*$/gm)]
|
|
if (signers.length !== 1 || signers[0][1].toLowerCase() !== expected) throw new Error('signing-certificate-mismatch')
|
|
if (/^Signer #\d+ certificate DN:.*\bCN=Android Debug\b/im.test(signature)) throw new Error('debug-signing-certificate')
|
|
if (!/^Verified using v[23](?:\.\d+)? scheme[^\r\n]*: true\s*$/m.test(signature)) throw new Error('modern-apk-signature-required')
|
|
const packages = [...badging.matchAll(/^package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'/gm)]
|
|
if (packages.length !== 1 || packages[0][1] !== 'online.turkservis.aiturk.mobile') throw new Error('application-id-mismatch')
|
|
if (packages[0][2] !== String(versionCode) || packages[0][3] !== version) throw new Error('apk-version-mismatch')
|
|
if (/^application-debuggable\b/m.test(badging)) throw new Error('debuggable-apk')
|
|
return { applicationId: packages[0][1], version, versionCode, certificateSha256: expected }
|
|
}
|
|
|
|
async function regularFile(path) {
|
|
let current = path
|
|
while (true) {
|
|
const info = await lstat(current)
|
|
if (info.isSymbolicLink()) throw new Error('apk-reparse-path')
|
|
if (current === path && !info.isFile()) throw new Error('apk-not-regular')
|
|
const parent = dirname(current)
|
|
if (parent === current) return
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
async function digest(path) {
|
|
const hash = createHash('sha256')
|
|
for await (const chunk of createReadStream(path)) hash.update(chunk)
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
function tool(executable, args) {
|
|
const env = { ...process.env }
|
|
delete env.JAVA_TOOL_OPTIONS
|
|
delete env._JAVA_OPTIONS
|
|
delete env.JDK_JAVA_OPTIONS
|
|
const result = spawnSync(executable, args, { env, encoding: 'utf8', windowsHide: true, timeout: 60_000, maxBuffer: 1024 * 1024 })
|
|
if (result.error || result.status !== 0) throw new Error('android-verification-tool-failed')
|
|
return result.stdout
|
|
}
|
|
|
|
export async function verifyAndroidRelease(options) {
|
|
const apk = resolve(options.apk)
|
|
await regularFile(apk)
|
|
const before = await lstat(apk)
|
|
const sha256 = await digest(apk)
|
|
const signature = tool(resolve(options.java), ['-jar', resolve(options.apksigner), 'verify', '--verbose', '--print-certs', apk])
|
|
const badging = tool(resolve(options.aapt2), ['dump', 'badging', apk])
|
|
const identity = validateAndroidRelease({ ...options, signature, badging })
|
|
await regularFile(apk)
|
|
const after = await lstat(apk)
|
|
if (before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || await digest(apk) !== sha256) throw new Error('apk-changed-during-verification')
|
|
return { verified: true, ...identity, size: after.size, sha256, deviceAcceptanceVerified: false }
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
const { values } = parseArgs({ options: Object.fromEntries(['apk', 'java', 'apksigner', 'aapt2', 'certificate', 'version', 'version-code'].map(name => [name, { type: 'string' }])) })
|
|
try {
|
|
if (Object.keys(values).length !== 7) throw new Error('all-release-verification-arguments-required')
|
|
console.log(JSON.stringify(await verifyAndroidRelease({ ...values, versionCode: Number(values['version-code']) })))
|
|
} catch (error) {
|
|
const code = /^[a-z][a-z0-9-]{1,95}$/.test(error.message) ? error.message : 'android-release-verification-failed'
|
|
console.error(JSON.stringify({ verified: false, code }))
|
|
process.exitCode = 1
|
|
}
|
|
}
|