From 056e18484029ca72d424a46cc1c9142042eaa6de Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Sun, 7 Nov 2021 20:01:41 -0800 Subject: [PATCH] Add command to check presigned APKs --- src/blobs/file_list.ts | 33 +++-------------- src/commands/check-presigned.ts | 66 +++++++++++++++++++++++++++++++++ src/commands/extract.ts | 4 +- src/sepolicy/parse_seapp.ts | 31 ++++++++++++++++ src/util/fs.ts | 25 +++++++++++++ 5 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 src/commands/check-presigned.ts create mode 100644 src/sepolicy/parse_seapp.ts create mode 100644 src/util/fs.ts diff --git a/src/blobs/file_list.ts b/src/blobs/file_list.ts index 02da84a..621181f 100644 --- a/src/blobs/file_list.ts +++ b/src/blobs/file_list.ts @@ -1,10 +1,10 @@ -import { promises as fs } from 'fs' import * as path from 'path' import * as chalk from 'chalk' import * as ora from 'ora' import { EXT_PARTITIONS } from '../partitions' import { BlobEntry } from './entry' +import { exists, listFilesRecursive } from '../util/fs' // Sub-partition directories to ignore const IGNORE_DIRS = new Set([ @@ -152,6 +152,10 @@ system_ext/priv-app/FactoryOta/FactoryOta.apk`, product/wallpaper/`, ]) +function paths(blocks: Array) { + return blocks.flatMap(b => b.split('\n')) +} + export function parseFileList(list: string) { let entries = [] @@ -195,33 +199,6 @@ export function parseFileList(list: string) { return entries.sort((a, b) => a.srcPath.localeCompare(b.srcPath)) } -function paths(blocks: Array) { - return blocks.flatMap(b => b.split('\n')) -} - -// https://stackoverflow.com/a/45130990 -async function* listFilesRecursive(dir: string): AsyncGenerator { - const dirents = await fs.readdir(dir, { withFileTypes: true }) - for (const dirent of dirents) { - const res = path.resolve(dir, dirent.name) - if (dirent.isDirectory()) { - yield* listFilesRecursive(res) - } else if (dirent.isFile() || dirent.isSymbolicLink()) { - yield res - } - } -} - -async function exists(path: string) { - try { - await fs.access(path) - return true - } catch { - // Doesn't exist or can't read - return false - } -} - export async function listPart(partition: string, systemRoot: string) { let partRoot = `${systemRoot}/${partition}` if (!await exists(partRoot)) { diff --git a/src/commands/check-presigned.ts b/src/commands/check-presigned.ts new file mode 100644 index 0000000..dbfa8cb --- /dev/null +++ b/src/commands/check-presigned.ts @@ -0,0 +1,66 @@ +import { Command, flags } from '@oclif/command' +import { promises as fs } from 'fs' +import * as path from 'path' +import * as chalk from 'chalk' +import { $ } from 'zx' + +import { parseFileList } from '../blobs/file_list' +import { listFilesRecursive } from '../util/fs' +import { parseSeappContexts } from '../sepolicy/parse_seapp' + +$.verbose = false + +export default class CheckPresigned extends Command { + static description = 'check for APKs that should be presigned' + + static flags = { + help: flags.help({char: 'h'}), + aapt2: flags.string({char: 'a', description: 'path to aapt2 executable', default: 'out/host/linux-x86/bin/aapt2'}), + sepolicy: flags.string({char: 's', description: 'paths to device and vendor sepolicy dirs', required: true, multiple: true}), + } + + static args = [ + {name: 'source', description: 'path to mounted factory images', required: true}, + {name: 'listPath', description: 'path to LineageOS-compatible proprietary-files.txt list', required: true}, + ] + + async run() { + let {flags: {aapt2, sepolicy}, args: {source, listPath}} = this.parse(CheckPresigned) + + // Parse list + this.log(chalk.bold(chalk.greenBright('Parsing list'))) + let list = await fs.readFile(listPath, {encoding: 'utf8'}) + let entries = parseFileList(list) + + // Find and parse sepolicy seapp_contexts + let contexts = [] + for (let dir of sepolicy) { + for await (let file of listFilesRecursive(dir)) { + if (path.basename(file) != 'seapp_contexts') { + continue + } + + let rawContexts = await fs.readFile(file, { encoding: 'utf8' }) + contexts.push(...parseSeappContexts(rawContexts)) + } + } + let presignedPkgs = new Set(contexts.filter(c => c.seinfo != 'platform') + .map(c => c.name)) + + // Process APKs + for (let entry of entries) { + if (path.extname(entry.path) != '.apk') { + continue + } + + let procOut = await $`${aapt2} dump packagename ${source}/${entry.srcPath}` + let pkgName = procOut.stdout.trim() + if (presignedPkgs.has(pkgName)) { + entry.isPresigned = true + this.log(entry.srcPath) + } + } + + // TODO: write new list + } +} diff --git a/src/commands/extract.ts b/src/commands/extract.ts index 7db2d59..287c592 100644 --- a/src/commands/extract.ts +++ b/src/commands/extract.ts @@ -29,16 +29,16 @@ export default class Extract extends Command { help: flags.help({char: 'h'}), vendor: flags.string({char: 'v', description: 'device vendor/OEM name', required: true}), device: flags.string({char: 'd', description: 'device codename', required: true}), - source: flags.string({char: 's', description: 'path to mounted factory images', required: true}), skipCopy: flags.boolean({char: 'k', description: 'skip file copying and only generate build files'}), } static args = [ + {name: 'source', description: 'path to mounted factory images', required: true}, {name: 'listPath', description: 'path to LineageOS-compatible proprietary-files.txt list', required: true}, ] async run() { - let {args: {listPath}, flags: {vendor, device, source, skipCopy}} = this.parse(Extract) + let {args: {source, listPath}, flags: {vendor, device, skipCopy}} = this.parse(Extract) // Parse list this.log(chalk.bold(chalk.greenBright('Parsing list'))) diff --git a/src/sepolicy/parse_seapp.ts b/src/sepolicy/parse_seapp.ts new file mode 100644 index 0000000..f3c546e --- /dev/null +++ b/src/sepolicy/parse_seapp.ts @@ -0,0 +1,31 @@ +export interface SeappContext { + user: string + seinfo?: string + isPrivApp?: boolean + name: string + domain: string + type?: string + levelFrom: string +} + +export function parseSeappContexts(seappContexts: string) { + let contexts = [] + for (let line of seappContexts.split('\n')) { + // Ignore comments and empty/blank lines + if (line.length == 0 || line.startsWith('#') || line.match(/^\s*$/)) { + continue + } + + // Parse key-value fields + let rawContext: { [key: string]: string } = {} + for (let kv of line.trim().split(/\s+/)) { + let [key, value] = kv.split('=') + rawContext[key] = value + } + + // Cast directly to object + contexts.push(rawContext as unknown as SeappContext) + } + + return contexts +} diff --git a/src/util/fs.ts b/src/util/fs.ts new file mode 100644 index 0000000..ae35a04 --- /dev/null +++ b/src/util/fs.ts @@ -0,0 +1,25 @@ +import { promises as fs } from 'fs' +import * as path from 'path' + +// https://stackoverflow.com/a/45130990 +export async function* listFilesRecursive(dir: string): AsyncGenerator { + const dirents = await fs.readdir(dir, { withFileTypes: true }) + for (const dirent of dirents) { + const res = path.resolve(dir, dirent.name) + if (dirent.isDirectory()) { + yield* listFilesRecursive(res) + } else if (dirent.isFile() || dirent.isSymbolicLink()) { + yield res + } + } +} + +export async function exists(path: string) { + try { + await fs.access(path) + return true + } catch { + // Doesn't exist or can't read + return false + } +}