diff --git a/README.md b/README.md index 7e8c75a..b26c66c 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,6 @@ Additionally, you will need to [prepare the target device/emulator](#device-prep If you want to work with physical devices, [some setup may be necessary depending on your system](https://developer.android.com/studio/run/device#setting-up). On Ubuntu, you need to be a member of the `plugdev` group (`sudo usermod -aG plugdev `) and have `udev` rules for your device (`sudo apt install android-sdk-platform-tools-common`). For other distributions, see [android-udev-rules](https://github.com/M0Rf30/android-udev-rules). -You also need `openssl`. - ### Host dependencies for iOS For iOS, you need [`libimobiledevice`](https://libimobiledevice.org/) and `ideviceinstaller`. The distribution packages are fine, if available. On Windows, you will additionally need the Apple Device Driver and the Apple Application Support service. You can get those by installing iTunes. diff --git a/docs/README.md b/docs/README.md index 12dd8ed..7874e0a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,7 +42,7 @@ An ID of a known permission on Android. #### Defined in -[android.ts:876](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L876) +[android.ts:882](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L882) ___ @@ -112,7 +112,7 @@ An ID of a known permission on iOS. #### Defined in -[ios.ts:397](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L397) +[ios.ts:393](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L393) ___ @@ -320,7 +320,7 @@ The IDs of known permissions on Android. #### Defined in -[android.ts:745](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L745) +[android.ts:751](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L751) ___ @@ -332,7 +332,7 @@ The IDs of known permissions on iOS. #### Defined in -[ios.ts:380](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L380) +[ios.ts:376](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L376) ## Functions @@ -371,7 +371,7 @@ An object with the properties listed above, or `undefined` if the file doesn't e #### Defined in -[util.ts:67](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L67) +[util.ts:68](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L68) ___ @@ -393,7 +393,7 @@ Pause for a given duration. #### Defined in -[util.ts:44](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L44) +[util.ts:45](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L45) ___ diff --git a/src/android.ts b/src/android.ts index fadc29c..f026511 100644 --- a/src/android.ts +++ b/src/android.ts @@ -2,7 +2,7 @@ import { decompress as decompressXz } from '@napi-rs/lzma/xz'; import { runAndroidDevTool } from 'andromatic'; import { getVenv } from 'autopy'; import fetch from 'cross-fetch'; -import { execa } from 'execa'; +import { createHash } from 'crypto'; import { fileTypeFromFile } from 'file-type'; import frida from 'frida'; import { open, rm, writeFile } from 'fs/promises'; @@ -28,6 +28,7 @@ import { getObjFromFridaScript, isRecord, parseAppMeta, + parsePemCertificateFromFile, retryCondition, tmpFileFromZipEntry, } from './util'; @@ -173,12 +174,17 @@ export const androidApi = >( }); }, - getCertificateSubjectHashOld: (path: string) => - execa('openssl', ['x509', '-inform', 'PEM', '-subject_hash_old', '-in', path]).then( - // The `trim()` is necessary for Windows: - // https://github.com/tweaselORG/meta/issues/25#issuecomment-1507665763 - ({ stdout }) => stdout.split('\n')[0]?.trim() - ), + // This imitates `openssl x509 -inform PEM -subject_hash_old -in `. + // See: https://github.com/tweaselORG/appstraction/issues/79 + getCertificateSubjectHashOld: async (path: string) => { + const { cert } = await parsePemCertificateFromFile(path); + + const hash = createHash('md5').update(Buffer.from(cert.subject.valueBeforeDecode)).digest(); + const truncated = hash.subarray(0, 4); + const ulong = (truncated[0]! | (truncated[1]! << 8) | (truncated[2]! << 16) | (truncated[3]! << 24)) >>> 0; + + return ulong.toString(16); + }, hasCertificateAuthority: (filename) => adb(['shell', 'ls', `/system/etc/security/cacerts/${filename}`], { reject: false }).then( ({ exitCode }) => exitCode === 0 diff --git a/src/ios.ts b/src/ios.ts index 26e5905..a30d6ae 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -2,12 +2,16 @@ import { getVenv } from 'autopy'; import { createHash } from 'crypto'; import { execa } from 'execa'; import frida from 'frida'; -import { readFile } from 'fs/promises'; import { NodeSSH } from 'node-ssh'; -import { Certificate } from 'pkijs'; import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.'; import { venvOptions } from '../scripts/common/python'; -import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './util'; +import { + asyncUnimplemented, + getObjFromFridaScript, + isRecord, + parsePemCertificateFromFile, + retryCondition, +} from './util'; const venv = getVenv(venvOptions); const python = async (...args: Parameters>) => (await venv)(...args); @@ -316,16 +320,10 @@ export const iosApi = >( if (!options.capabilities.includes('ssh')) throw new Error('SSH is required for installing a certificate authority.'); - const certPem = await readFile(path, 'utf8'); - - // A PEM certificate is just a base64-encoded DER certificate with a header and footer. - const certBase64 = certPem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\r\n])/g, ''); - const certDer = Buffer.from(certBase64, 'base64'); - - const c = Certificate.fromBER(certDer); + const { cert, certDer } = await parsePemCertificateFromFile(path); const sha256 = createHash('sha256').update(certDer).digest('hex'); - const subj = Buffer.from(c.subject.toSchema().valueBlock.toBER()).toString('hex'); + const subj = Buffer.from(cert.subject.toSchema().valueBlock.toBER()).toString('hex'); const tset = Buffer.from( ` @@ -343,9 +341,7 @@ export const iosApi = >( if (!options.capabilities.includes('ssh')) throw new Error('SSH is required for removing a certificate authority.'); - const certPem = await readFile(path, 'utf8'); - const certBase64 = certPem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\r\n])/g, ''); - const certDer = Buffer.from(certBase64, 'base64'); + const { certDer } = await parsePemCertificateFromFile(path); const sha256 = createHash('sha256').update(certDer).digest('hex'); await this._internal.ssh( diff --git a/src/util.ts b/src/util.ts index cb80d13..a6e0a60 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,8 +5,9 @@ import frida from 'frida'; import { createWriteStream } from 'fs'; import fs from 'fs-extra'; import type { FileHandle } from 'fs/promises'; -import { open } from 'fs/promises'; +import { open, readFile } from 'fs/promises'; import _ipaInfo from 'ipa-extract-info'; +import { Certificate } from 'pkijs'; import type { Readable } from 'stream'; import { temporaryFile } from 'tempy'; import type { Entry, ZipFile } from 'yauzl'; @@ -313,3 +314,13 @@ export type XapkManifest = { expansions?: { file: string; install_location: string; install_path: string }[]; split_apks?: { file: string; id: string }[]; }; + +export const parsePemCertificateFromFile = async (path: string) => { + const certPem = await readFile(path, 'utf8'); + + // A PEM certificate is just a base64-encoded DER certificate with a header and footer. + const certBase64 = certPem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\r\n])/g, ''); + const certDer = Buffer.from(certBase64, 'base64'); + + return { cert: Certificate.fromBER(certDer), certPem, certDer }; +};