From 074acee70598533668096ff28e659e7c8505a7f5 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 25 May 2023 16:19:16 +0200 Subject: [PATCH 1/8] Split up util.ts --- docs/README.md | 28 ++++----- src/android.ts | 8 +-- src/index.ts | 4 +- src/ios.ts | 9 +-- src/{util.ts => utils/index.ts} | 108 +------------------------------- src/utils/zip.ts | 104 ++++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 133 deletions(-) rename src/{util.ts => utils/index.ts} (72%) create mode 100644 src/utils/zip.ts diff --git a/docs/README.md b/docs/README.md index 5d32b47..43f47ef 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:913](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L913) +[android.ts:943](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L943) ___ @@ -81,7 +81,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the #### Defined in -[index.ts:400](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L400) +[index.ts:404](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L404) ___ @@ -100,7 +100,7 @@ The options for each attribute available through the `getDeviceAttribute()` func #### Defined in -[index.ts:406](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L406) +[index.ts:410](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L410) ___ @@ -112,7 +112,7 @@ An ID of a known permission on iOS. #### Defined in -[ios.ts:455](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L455) +[ios.ts:450](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L450) ___ @@ -200,7 +200,7 @@ The options for the `platformApi()` function. #### Defined in -[index.ts:338](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L338) +[index.ts:342](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L342) ___ @@ -219,7 +219,7 @@ Connection details for a proxy. #### Defined in -[index.ts:414](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L414) +[index.ts:418](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L418) ___ @@ -249,7 +249,7 @@ The options for a specific platform/run target combination. #### Defined in -[index.ts:365](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L365) +[index.ts:369](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L369) ___ @@ -267,7 +267,7 @@ A capability for the `platformApi()` function. #### Defined in -[index.ts:393](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L393) +[index.ts:397](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L397) ___ @@ -309,7 +309,7 @@ Configuration string for WireGuard. #### Defined in -[index.ts:421](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L421) +[index.ts:425](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L425) ## Variables @@ -321,7 +321,7 @@ The IDs of known permissions on Android. #### Defined in -[android.ts:782](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L782) +[android.ts:812](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L812) ___ @@ -333,7 +333,7 @@ The IDs of known permissions on iOS. #### Defined in -[ios.ts:438](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L438) +[ios.ts:433](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L433) ## Functions @@ -372,7 +372,7 @@ An object with the properties listed above, or `undefined` if the file doesn't e #### Defined in -[util.ts:68](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L68) +[utils/index.ts:64](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L64) ___ @@ -394,7 +394,7 @@ Pause for a given duration. #### Defined in -[util.ts:45](https://github.com/tweaselORG/appstraction/blob/main/src/util.ts#L45) +[utils/index.ts:41](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L41) ___ @@ -426,4 +426,4 @@ The API object for the given platform and run target. #### Defined in -[index.ts:430](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L430) +[index.ts:434](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L434) diff --git a/src/android.ts b/src/android.ts index 8b31d27..5d577ec 100644 --- a/src/android.ts +++ b/src/android.ts @@ -20,20 +20,18 @@ import type { } from '.'; import { dependencies } from '../package.json'; import { venvOptions } from '../scripts/common/python'; -import type { ParametersExceptFirst, XapkManifest } from './util'; +import type { ParametersExceptFirst, XapkManifest } from './utils'; import { asyncUnimplemented, escapeArg, escapeCommand, - forEachInZip, - getFileFromZip, getObjFromFridaScript, isRecord, parseAppMeta, parsePemCertificateFromFile, retryCondition, - tmpFileFromZipEntry, -} from './util'; +} from './utils'; +import { forEachInZip, getFileFromZip, tmpFileFromZipEntry } from './utils/zip'; const adb = (...args: ParametersExceptFirst) => runAndroidDevTool('adb', args[0], args[1]); const venv = getVenv(venvOptions); diff --git a/src/index.ts b/src/index.ts index 93ce20b..00be61e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import type { AndroidPermission } from './android'; import { androidApi } from './android'; import type { IosPermission } from './ios'; import { iosApi } from './ios'; -import type { ParametersExceptFirst } from './util'; +import type { ParametersExceptFirst } from './utils'; /** A platform that is supported by this library. */ export type SupportedPlatform = 'android' | 'ios'; @@ -450,5 +450,5 @@ export function platformApi< export { androidPermissions } from './android'; export { iosPermissions } from './ios'; -export { parseAppMeta, pause } from './util'; +export { parseAppMeta, pause } from './utils'; export { IosPermission, AndroidPermission }; diff --git a/src/ios.ts b/src/ios.ts index f102d6e..8d8f2d2 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -4,13 +4,8 @@ import frida from 'frida'; import { NodeSSH } from 'node-ssh'; import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.'; import { venvOptions } from '../scripts/common/python'; -import { - asyncUnimplemented, - getObjFromFridaScript, - isRecord, - parsePemCertificateFromFile, - retryCondition, -} from './util'; + +import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition,parsePemCertificateFromFile } from './utils'; const venv = getVenv(venvOptions); const python = async (...args: Parameters>) => (await venv)(...args); diff --git a/src/util.ts b/src/utils/index.ts similarity index 72% rename from src/util.ts rename to src/utils/index.ts index b334b42..2b5c4b9 100644 --- a/src/util.ts +++ b/src/utils/index.ts @@ -2,17 +2,13 @@ import { runAndroidDevTool } from 'andromatic'; import { fileTypeFromFile } from 'file-type'; import type { TargetProcess } from 'frida'; import frida from 'frida'; -import { createWriteStream } from 'fs'; import fs from 'fs-extra'; -import type { FileHandle } 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'; -import { fromFd } from 'yauzl'; -import type { AppPath, SupportedPlatform } from './index'; + +import type { AppPath, SupportedPlatform } from '../index'; +import { getFileFromZip, writeFileFromZipToTmp } from './zip'; // eslint-disable-next-line @typescript-eslint/no-empty-function export const asyncNop = async () => {}; @@ -208,104 +204,6 @@ export const getObjFromFridaScript = async (targetProcess: TargetProcess | undef export const isRecord = (maybeRecord: unknown): maybeRecord is Record => !!maybeRecord && typeof maybeRecord === 'object'; -/** - * Promise wrapper for yauzl.fromFd. - * - * @param zip FileHandle of the zip file. - * - * @returns ZipFile to be used with yauzl. - */ -export const openZipFile = async (zip: FileHandle) => - new Promise((resolve) => { - fromFd(zip.fd, { lazyEntries: true }, (err, zipFile) => { - if (err) throw err; - resolve(zipFile); - }); - }); - -/** - * Run a function on each entry in a zip file. Resolves if all entries have been processed. - * - * @param zip FileHandle of the zip file. - * @param callback Function to run on each entry, it will receive the entry and a reference to the current ZipFile. - */ -export const forEachInZip = async (zip: FileHandle, callback: (entry: Entry, zipFile: ZipFile) => Promise) => - openZipFile(zip).then( - (zipFile) => - new Promise((resolve) => { - zipFile.readEntry(); - zipFile.on('entry', (entry: Entry) => { - callback(entry, zipFile).then(() => zipFile.readEntry()); - }); - zipFile.on('end', () => resolve()); - }) - ); - -/** - * Get a Readable stream of a file entry in a zip file. - * - * @param zip FileHandle of the zip file. - * @param filename Name of the file entry in the zip. - * - * @returns A Readable stream of the file entry, or void if the file entry was not found. - */ -export const getFileFromZip = async (zip: FileHandle, filename: string) => - openZipFile(zip).then( - (zipFile) => - new Promise((resolve) => { - zipFile.readEntry(); - zipFile.on('entry', (entry: Entry) => { - if (entry.fileName !== filename) { - zipFile.readEntry(); - return; - } - zipFile.openReadStream(entry, (err, stream) => { - if (err) throw err; - resolve(stream); - }); - }); - zipFile.on('end', () => resolve()); - }) - ); - -export const writeFileFromZipToTmp = async (zip: FileHandle, filename: string) => - openZipFile(zip).then( - (zipFile) => - new Promise((resolve) => { - zipFile.readEntry(); - zipFile.on('entry', (entry: Entry) => { - if (entry.fileName !== filename) { - zipFile.readEntry(); - return; - } - tmpFileFromZipEntry(zipFile, entry).then((tmpFile) => resolve(tmpFile)); - }); - zipFile.on('end', () => resolve()); - }) - ); - -/** - * Write the contents of a zip entry to a temporary file. - * - * @param zipFile Yauzl ZipFile to read from. - * @param entry Entry in the zip file. - * @param extension Optional file extension to use for the temporary file. - * - * @returns The file name of the temporary file. - */ -export const tmpFileFromZipEntry = async ( - zipFile: ZipFile, - entry: Entry, - extension?: Extension -) => - new Promise<`${string}.${Extension}`>((resolve) => { - zipFile.openReadStream(entry, (err, stream) => { - if (err) throw Error; - const tmpFile = temporaryFile({ extension }) as `${string}.${Extension}`; - stream.pipe(createWriteStream(tmpFile).on('finish', () => resolve(tmpFile))); - }); - }); - // Taken from: https://stackoverflow.com/a/67605309 // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ParametersExceptFirst = F extends (arg0: any, ...rest: infer R) => any ? R : never; diff --git a/src/utils/zip.ts b/src/utils/zip.ts new file mode 100644 index 0000000..34e269f --- /dev/null +++ b/src/utils/zip.ts @@ -0,0 +1,104 @@ +import { createWriteStream } from 'fs'; +import type { FileHandle } from 'fs/promises'; +import type { Readable } from 'stream'; +import { temporaryFile } from 'tempy'; +import type { Entry, ZipFile } from 'yauzl'; +import { fromFd } from 'yauzl'; + +/** + * Promise wrapper for yauzl.fromFd. + * + * @param zip FileHandle of the zip file. + * + * @returns ZipFile to be used with yauzl. + */ +export const openZipFile = async (zip: FileHandle) => + new Promise((resolve) => { + fromFd(zip.fd, { lazyEntries: true }, (err, zipFile) => { + if (err) throw err; + resolve(zipFile); + }); + }); + +/** + * Run a function on each entry in a zip file. Resolves if all entries have been processed. + * + * @param zip FileHandle of the zip file. + * @param callback Function to run on each entry, it will receive the entry and a reference to the current ZipFile. + */ +export const forEachInZip = async (zip: FileHandle, callback: (entry: Entry, zipFile: ZipFile) => Promise) => + openZipFile(zip).then( + (zipFile) => + new Promise((resolve) => { + zipFile.readEntry(); + zipFile.on('entry', (entry: Entry) => { + callback(entry, zipFile).then(() => zipFile.readEntry()); + }); + zipFile.on('end', () => resolve()); + }) + ); + +/** + * Get a Readable stream of a file entry in a zip file. + * + * @param zip FileHandle of the zip file. + * @param filename Name of the file entry in the zip. + * + * @returns A Readable stream of the file entry, or void if the file entry was not found. + */ +export const getFileFromZip = async (zip: FileHandle, filename: string) => + openZipFile(zip).then( + (zipFile) => + new Promise((resolve) => { + zipFile.readEntry(); + zipFile.on('entry', (entry: Entry) => { + if (entry.fileName !== filename) { + zipFile.readEntry(); + return; + } + zipFile.openReadStream(entry, (err, stream) => { + if (err) throw err; + resolve(stream); + }); + }); + zipFile.on('end', () => resolve()); + }) + ); + +export const writeFileFromZipToTmp = async (zip: FileHandle, filename: string) => + openZipFile(zip).then( + (zipFile) => + new Promise((resolve) => { + zipFile.readEntry(); + zipFile.on('entry', (entry: Entry) => { + if (entry.fileName !== filename) { + zipFile.readEntry(); + return; + } + tmpFileFromZipEntry(zipFile, entry).then((tmpFile) => resolve(tmpFile)); + }); + zipFile.on('end', () => resolve()); + }) + ); + +/** + * Write the contents of a zip entry to a temporary file. + * + * @param zipFile Yauzl ZipFile to read from. + * @param entry Entry in the zip file. + * @param extension Optional file extension to use for the temporary file. + * + * @returns The file name of the temporary file. + */ +export const tmpFileFromZipEntry = async ( + zipFile: ZipFile, + entry: Entry, + extension?: Extension +) => + new Promise<`${string}.${Extension}`>((resolve) => { + zipFile.openReadStream(entry, (err, stream) => { + if (err) throw Error; + const tmpFile = temporaryFile({ extension }) as `${string}.${Extension}`; + stream.pipe(createWriteStream(tmpFile).on('finish', () => resolve(tmpFile))); + }); + }); From 33f32880cc010fd1b072d68e553930111587c49d Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 25 May 2023 16:25:39 +0200 Subject: [PATCH 2/8] Add utility functions to generate certificates --- package.json | 4 ++ src/android.ts | 2 +- src/ios.ts | 3 +- src/utils/crypto.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++ src/utils/index.ts | 13 +---- 5 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 src/utils/crypto.ts diff --git a/package.json b/package.json index c84eba8..5da86cf 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@napi-rs/lzma": "^1.1.2", "andromatic": "^1.0.0", "autopy": "^1.1.1", + "asn1js": "^3.0.5", "cross-fetch": "^3.1.5", "execa": "^6.1.0", "file-type": "^18.3.0", @@ -94,5 +95,8 @@ "typedoc": "^0.23.26", "typedoc-plugin-markdown": "3.14.0", "typescript": "4.9.4" + }, + "engines": { + "node": ">=15.0.0" } } diff --git a/src/android.ts b/src/android.ts index 5d577ec..19a74e3 100644 --- a/src/android.ts +++ b/src/android.ts @@ -28,10 +28,10 @@ import { getObjFromFridaScript, isRecord, parseAppMeta, - parsePemCertificateFromFile, retryCondition, } from './utils'; import { forEachInZip, getFileFromZip, tmpFileFromZipEntry } from './utils/zip'; +import { parsePemCertificateFromFile} from './utils/crypto'; const adb = (...args: ParametersExceptFirst) => runAndroidDevTool('adb', args[0], args[1]); const venv = getVenv(venvOptions); diff --git a/src/ios.ts b/src/ios.ts index 8d8f2d2..d4dd319 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -4,8 +4,9 @@ import frida from 'frida'; import { NodeSSH } from 'node-ssh'; import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.'; import { venvOptions } from '../scripts/common/python'; +import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils'; +import { parsePemCertificateFromFile} from './utils/crypto'; -import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition,parsePemCertificateFromFile } from './utils'; const venv = getVenv(venvOptions); const python = async (...args: Parameters>) => (await venv)(...args); diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts new file mode 100644 index 0000000..97ba2ee --- /dev/null +++ b/src/utils/crypto.ts @@ -0,0 +1,131 @@ +import { BmpString, Integer } from 'asn1js'; +import { webcrypto } from 'crypto'; +import { readFile } from 'fs/promises'; +import { + AttributeTypeAndValue, + AuthenticatedSafe, + CertBag, + Certificate, + CryptoEngine, + PFX, + PrivateKeyInfo, + SafeBag, + SafeContents, +} from 'pkijs'; + +const crypto = new CryptoEngine({ name: 'node-webcrypto', crypto: webcrypto as Crypto }); + +export const generateCertificate = async (commonName: string, days?: number) => { + const algorithm = crypto.getAlgorithmParameters('ecdsa', 'generateKey'); + const { privateKey, publicKey } = await crypto.generateKey( + algorithm.algorithm as EcKeyAlgorithm, + true, + algorithm.usages + ); + + const cert = new Certificate(); + cert.version = 2; + cert.serialNumber = new Integer({ value: Date.now() }); + cert.notBefore.value = new Date(); + cert.notAfter.value = new Date(); + cert.notAfter.value.setDate(cert.notBefore.value.getDate() + (days || 365)); + + cert.issuer.typesAndValues.push( + new AttributeTypeAndValue({ + type: '2.5.4.3', // Common name + value: new BmpString({ value: commonName }), + }) + ); + cert.subject.typesAndValues.push( + new AttributeTypeAndValue({ + type: '2.5.4.3', // Common name + value: new BmpString({ value: commonName }), + }) + ); + + await cert.subjectPublicKeyInfo.importKey(publicKey, crypto); + await cert.sign(privateKey, 'SHA-256', crypto); + + return { + certificate: cert.toSchema().toBER(false), + privateKey: await crypto.exportKey('pkcs8', privateKey), + }; +}; +export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, password?: string) => { + const pkcs12 = new PFX({ + parsedValue: { + integrityMode: 0, // Password-Based Integrity Mode + authenticatedSafe: new AuthenticatedSafe({ + parsedValue: { + safeContents: [ + { + privacyMode: password ? 1 : 0, // 1 - Password based privacy mode, 0 - No privacy mode + value: new SafeContents({ + safeBags: [ + new SafeBag({ + bagId: '1.2.840.113549.1.12.10.1.1', // Private key bag + bagValue: PrivateKeyInfo.fromBER(key), + }), + new SafeBag({ + bagId: '1.2.840.113549.1.12.10.1.3', // Certificate bag + bagValue: new CertBag({ + parsedValue: Certificate.fromBER(cert), + }), + }), + ], + }), + }, + ], + }, + }), + }, + }); + + if (!pkcs12.parsedValue?.authenticatedSafe) + throw new Error('Broken certificate container: pkcs12.parsedValue.authenticatedSafe is empty'); + + pkcs12.parsedValue.authenticatedSafe.makeInternalValues( + { + safeContents: password + ? [ + { + password: new TextEncoder().encode(password), + contentEncryptionAlgorithm: { + name: 'AES-CBC', + length: 128, + }, + hmacHashAlgorithm: 'SHA-256', + iterationCount: 2048, + }, + ] + : [{}], + }, + crypto + ); + + await pkcs12.makeInternalValues( + { + password: password || '', + iterations: 100000, + pbkdf2HashAlgorithm: 'SHA-256', + hmacHashAlgorithm: 'SHA-256', + }, + crypto + ); + return pkcs12.toSchema().toBER(); +}; + +export const arrayBufferToPem = (buffer: ArrayBuffer, tag: 'CERTIFICATE' | 'PRIVATE KEY' | 'PUBLIC KEY') => { + const base64 = Buffer.from(buffer).toString('base64'); + return `-----BEGIN ${tag}-----\n${base64.replace(/(.{64})/g, '$1\n').trim()}\n-----END ${tag}-----`; // Thanks Copilot! +}; + +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 }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 2b5c4b9..b55d335 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,9 +3,8 @@ import { fileTypeFromFile } from 'file-type'; import type { TargetProcess } from 'frida'; import frida from 'frida'; import fs from 'fs-extra'; -import { open, readFile } from 'fs/promises'; +import { open } from 'fs/promises'; import _ipaInfo from 'ipa-extract-info'; -import { Certificate } from 'pkijs'; import type { AppPath, SupportedPlatform } from '../index'; import { getFileFromZip, writeFileFromZipToTmp } from './zip'; @@ -213,16 +212,6 @@ export type XapkManifest = { 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 }; -}; - // I adapted this from https://github.com/sindresorhus/execa/blob/b44d4066aebd2db0c7864936e710b9fa6f5ab9d2/lib/command.js#L12-L25 const NO_ESCAPE_REGEXP = /^[\w.-]+$/; From 030f9555a6463f11e41abb2c6441acccb0316d6c Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Tue, 30 May 2023 23:04:10 +0200 Subject: [PATCH 3/8] Fixes #81: Implement a function to enable supervision mode on jailbroken devices --- docs/README.md | 24 ++++----- package.json | 3 ++ src/index.ts | 20 ++++++++ src/ios.ts | 122 ++++++++++++++++++++++++++++++++++++++++++-- src/utils/crypto.ts | 87 ++++++++++++++++++++++++------- yarn.lock | 21 +++++++- 6 files changed, 242 insertions(+), 35 deletions(-) diff --git a/docs/README.md b/docs/README.md index 43f47ef..4b21e54 100644 --- a/docs/README.md +++ b/docs/README.md @@ -81,7 +81,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the #### Defined in -[index.ts:404](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L404) +[index.ts:424](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L424) ___ @@ -100,7 +100,7 @@ The options for each attribute available through the `getDeviceAttribute()` func #### Defined in -[index.ts:410](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L410) +[index.ts:430](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L430) ___ @@ -112,7 +112,7 @@ An ID of a known permission on iOS. #### Defined in -[ios.ts:450](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L450) +[ios.ts:565](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L565) ___ @@ -200,7 +200,7 @@ The options for the `platformApi()` function. #### Defined in -[index.ts:342](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L342) +[index.ts:362](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L362) ___ @@ -219,7 +219,7 @@ Connection details for a proxy. #### Defined in -[index.ts:418](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L418) +[index.ts:438](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L438) ___ @@ -249,7 +249,7 @@ The options for a specific platform/run target combination. #### Defined in -[index.ts:369](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L369) +[index.ts:389](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L389) ___ @@ -267,7 +267,7 @@ A capability for the `platformApi()` function. #### Defined in -[index.ts:397](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L397) +[index.ts:417](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L417) ___ @@ -309,7 +309,7 @@ Configuration string for WireGuard. #### Defined in -[index.ts:425](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L425) +[index.ts:445](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L445) ## Variables @@ -333,7 +333,7 @@ The IDs of known permissions on iOS. #### Defined in -[ios.ts:433](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L433) +[ios.ts:548](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L548) ## Functions @@ -372,7 +372,7 @@ An object with the properties listed above, or `undefined` if the file doesn't e #### Defined in -[utils/index.ts:64](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L64) +[utils/index.ts:63](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L63) ___ @@ -394,7 +394,7 @@ Pause for a given duration. #### Defined in -[utils/index.ts:41](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L41) +[utils/index.ts:40](https://github.com/tweaselORG/appstraction/blob/main/src/utils/index.ts#L40) ___ @@ -426,4 +426,4 @@ The API object for the given platform and run target. #### Defined in -[index.ts:434](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L434) +[index.ts:454](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L454) diff --git a/package.json b/package.json index 5da86cf..7624276 100644 --- a/package.json +++ b/package.json @@ -57,11 +57,14 @@ "andromatic": "^1.0.0", "autopy": "^1.1.1", "asn1js": "^3.0.5", + "bplist-creator": "^0.1.1", + "bplist-parser": "^0.3.2", "cross-fetch": "^3.1.5", "execa": "^6.1.0", "file-type": "^18.3.0", "frida": "^16.0.8", "fs-extra": "^11.1.0", + "global-cache-dir": "^5.0.0", "ipa-extract-info": "^1.2.6", "node-ssh": "^13.1.0", "p-retry": "^5.1.2", diff --git a/src/index.ts b/src/index.ts index 00be61e..b19b543 100644 --- a/src/index.ts +++ b/src/index.ts @@ -326,6 +326,26 @@ export type PlatformApi< */ setupEnvironment: () => Promise; ensureFrida: () => Promise; + /** + * Ensures that the current host is configured to supervise the connected device. If this is not the case, + * it sets the host to be the (only) supervisor by installing its certificate on the device. This will + * overwrite parts of the exisiting CloudConfiguration. If there is no host certificate, yet, or it has + * expired, it will be generated. + * + * Might restart the device, if a new cofniguration is pushed. You are adviced to wait for the device. + */ + ensureSupervision: () => Promise; + /** + * Removes all configured supervision hosts from the device. + * + * Will restart the device. You are adviced to wait for the device. + */ + removeSupervision: () => Promise; + /** + * Restarts the device only in the userspace, e.g. to keep the jailbroken kernel running. You might want + * to wait for the device to ensure it is available again. + */ + userspaceRestart: () => Promise; } : never; }; diff --git a/src/ios.ts b/src/ios.ts index d4dd319..52c5cd5 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -1,12 +1,24 @@ import { getVenv } from 'autopy'; +import bplist from 'bplist-creator'; +import { parseFile } from 'bplist-parser'; import { createHash } from 'crypto'; import frida from 'frida'; +import { exists,mkdirp } from 'fs-extra'; +import { readFile,writeFile } from 'fs/promises'; +import globalCacheDir from 'global-cache-dir'; import { NodeSSH } from 'node-ssh'; -import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.'; +import { join } from 'path'; +import type { PlatformApi,PlatformApiOptions,Proxy,SupportedCapability,SupportedRunTarget } from '.'; import { venvOptions } from '../scripts/common/python'; -import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils'; -import { parsePemCertificateFromFile} from './utils/crypto'; - +import { asyncUnimplemented,getObjFromFridaScript,isRecord,retryCondition } from './utils'; +import { +arrayBufferToPem, +certificateFingerprint, +certificateHasExpired, +generateCertificate, +parsePemCertificateFromFile, +pemToArrayBuffer +} from './utils/crypto'; const venv = getVenv(venvOptions); const python = async (...args: Parameters>) => (await venv)(...args); @@ -121,6 +133,8 @@ function getProxySettingsForCurrentWifiNetwork() { send({ name: "get_obj_from_frida_script", payload: getProxySettingsForCurrentWifiNetwork() });`, } as const; +const cloudConfigPath = + '/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/CloudConfigurationDetails.plist' as const; export const iosApi = >( options: PlatformApiOptions<'ios', RunTarget, SupportedCapability<'ios'>[]> @@ -206,6 +220,99 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); }); await session.detach(); }, + async ensureSupervision() { + if (!options.capabilities.includes('ssh')) + throw new Error('SSH is currently required to ensure supervison mode.'); + + const OrganizationName = 'appstraction'; + const cacheDir = await globalCacheDir('appstraction'); + + const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`); + const plist = (await parseFile(Buffer.from(encodedPlist, 'base64')))?.[0] as + | CloudConfigurationDetails + | undefined; + + if (!plist) throw new Error('Failed to ensure supervision mode: Invalid CloudConfiguration.'); + + let hostCert; + let hostKey; + + if ( + (await exists(join(cacheDir, 'ios', 'supervisorCert.pem'))) && + (await exists(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))) + ) { + hostCert = pemToArrayBuffer((await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString()); + hostKey = pemToArrayBuffer( + (await readFile(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))).toString() + ); + + if (!(await certificateHasExpired(hostCert))) { + const hostCertFingerprint = await certificateFingerprint(hostCert); + + // Test if the current host certificate is already controlling the device. + if ( + plist.IsSupervised && + plist.SupervisorHostCertificates && + plist.SupervisorHostCertificates.length > 0 && + plist.SupervisorHostCertificates.some( + async (cert) => (await certificateFingerprint(cert)) === hostCertFingerprint + ) + ) + return; + } else { + hostCert = undefined; + hostKey = undefined; + } + } + + if (!hostCert || !hostKey) { + // We have no exsiting keys, so let’s generate one. + const generated = await generateCertificate(OrganizationName); + hostCert = generated.certificate; + hostKey = generated.privateKey; + + await mkdirp(join(cacheDir, 'ios')); + await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), arrayBufferToPem(hostCert, 'CERTIFICATE')); + await writeFile( + join(cacheDir, 'ios', 'supervisorPrivateKey.pem'), + arrayBufferToPem(hostKey, 'PRIVATE KEY') + ); + } + + const newPlist = { + ...plist, + SupervisorHostCertificates: [Buffer.from(hostCert)], + IsSupervised: true, + OrganizationName, + AllowPairing: true, + }; + + await this.ssh(`echo "${bplist(newPlist).toString('base64')}" | base64 -d > ${cloudConfigPath}`); + await this.userspaceRestart(); + }, + async removeSupervision() { + const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`); + const plist = (await parseFile(Buffer.from(encodedPlist, 'base64')))?.[0] as + | CloudConfigurationDetails + | undefined; + + if (!plist) throw new Error('Failed to remove supervision mode: Invalid CloudConfiguration.'); + const newPlist = { + ...plist, + SupervisorHostCertificates: [], + IsSupervised: false, + OrganizationName: '', + }; + + await this.ssh(`echo "${bplist(newPlist).toString('base64')}" | base64 -d > ${cloudConfigPath}`); + await this.userspaceRestart(); + }, + async userspaceRestart() { + if (!options.capabilities.includes('ssh')) + throw new Error('SSH is currently required to restart in userspace.'); + + await this.ssh('ldrestart'); + }, }, resetDevice: asyncUnimplemented('resetDevice') as never, @@ -430,6 +537,13 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); }, }); +type CloudConfigurationDetails = Partial<{ + AllowPairing: boolean; + IsSupervised: boolean; + OrganizationName: string; + SupervisorHostCertificates: Buffer[]; +}>; + /** The IDs of known permissions on iOS. */ export const iosPermissions = [ 'kTCCServiceLiverpool', diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index 97ba2ee..ed79a39 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -8,15 +8,18 @@ import { Certificate, CryptoEngine, PFX, + PKCS8ShroudedKeyBag, PrivateKeyInfo, SafeBag, SafeContents, + setEngine, } from 'pkijs'; const crypto = new CryptoEngine({ name: 'node-webcrypto', crypto: webcrypto as Crypto }); +setEngine('node-webcrypto', crypto); // We need to do this, because there is a bug in pkijs (https://github.com/PeculiarVentures/PKI.js/issues/379) export const generateCertificate = async (commonName: string, days?: number) => { - const algorithm = crypto.getAlgorithmParameters('ecdsa', 'generateKey'); + const algorithm = crypto.getAlgorithmParameters('RSA-PSS', 'generateKey'); const { privateKey, publicKey } = await crypto.generateKey( algorithm.algorithm as EcKeyAlgorithm, true, @@ -51,7 +54,24 @@ export const generateCertificate = async (commonName: string, days?: number) => privateKey: await crypto.exportKey('pkcs8', privateKey), }; }; + +export const certificateFingerprint = async (certificateBuffer: ArrayBuffer, hashAlgorithm?: 'SHA-256' | 'SHA-1') => { + const certificate = await Certificate.fromBER(certificateBuffer); + const hash = await crypto.digest( + hashAlgorithm || 'SHA-256', + certificate.subjectPublicKeyInfo.toSchema().toBER(false) + ); + return Buffer.from(hash).toString('hex'); +}; + +export const certificateHasExpired = async (certificateBuffer: ArrayBuffer) => { + const certificate = await Certificate.fromBER(certificateBuffer); + return certificate.notAfter.value < new Date(); +}; + export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, password?: string) => { + const encodedPassword = new TextEncoder().encode(password || '').buffer; + const pkcs12 = new PFX({ parsedValue: { integrityMode: 0, // Password-Based Integrity Mode @@ -59,13 +79,22 @@ export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, parsedValue: { safeContents: [ { - privacyMode: password ? 1 : 0, // 1 - Password based privacy mode, 0 - No privacy mode + privacyMode: 0, // 0 - No privacy mode value: new SafeContents({ safeBags: [ new SafeBag({ - bagId: '1.2.840.113549.1.12.10.1.1', // Private key bag - bagValue: PrivateKeyInfo.fromBER(key), + bagId: '1.2.840.113549.1.12.10.1.2', // Shrouded Private Key Bag + bagValue: new PKCS8ShroudedKeyBag({ + parsedValue: PrivateKeyInfo.fromBER(key), + }), }), + ], + }), + }, + { + privacyMode: 1, // 1 - Password based privacy mode, + value: new SafeContents({ + safeBags: [ new SafeBag({ bagId: '1.2.840.113549.1.12.10.1.3', // Certificate bag bagValue: new CertBag({ @@ -84,28 +113,42 @@ export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, if (!pkcs12.parsedValue?.authenticatedSafe) throw new Error('Broken certificate container: pkcs12.parsedValue.authenticatedSafe is empty'); + await pkcs12.parsedValue.authenticatedSafe.parsedValue.safeContents[0].value.safeBags[0].bagValue.makeInternalValues( + { + password: encodedPassword, + contentEncryptionAlgorithm: { + name: 'AES-CBC', // OpenSSL can only handle AES-CBC (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L438) + length: 128, + }, + hmacHashAlgorithm: 'SHA-1', // OpenSSL can only handle SHA-1 (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L441) + iterationCount: 100000, + }, + crypto + ); + pkcs12.parsedValue.authenticatedSafe.makeInternalValues( { - safeContents: password - ? [ - { - password: new TextEncoder().encode(password), - contentEncryptionAlgorithm: { - name: 'AES-CBC', - length: 128, - }, - hmacHashAlgorithm: 'SHA-256', - iterationCount: 2048, - }, - ] - : [{}], + safeContents: [ + { + // Private key contents are encrypted differently, so this needs to be empty. + }, + { + password: encodedPassword, + contentEncryptionAlgorithm: { + name: 'AES-CBC', + length: 128, + }, + hmacHashAlgorithm: 'SHA-1', + iterationCount: 100000, + }, + ], }, crypto ); await pkcs12.makeInternalValues( { - password: password || '', + password: encodedPassword, iterations: 100000, pbkdf2HashAlgorithm: 'SHA-256', hmacHashAlgorithm: 'SHA-256', @@ -129,3 +172,11 @@ export const parsePemCertificateFromFile = async (path: string) => { return { cert: Certificate.fromBER(certDer), certPem, certDer }; }; + +export const pemToArrayBuffer = (pem: string) => { + const base64 = pem + .replace(/-----BEGIN (.*)-----/, '') + .replace(/-----END (.*)-----/, '') + .replace(/\n/g, ''); + return Uint8Array.from(Buffer.from(base64, 'base64')).buffer; +}; diff --git a/yarn.lock b/yarn.lock index 5ff3b22..33df9b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1707,7 +1707,7 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== -big-integer@^1.6.7: +big-integer@1.6.x, big-integer@^1.6.7: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== @@ -1751,6 +1751,13 @@ bottleneck@^2.15.3: resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== +bplist-creator@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.1.tgz#ef638af058a7021e10ebfd557ffd73d95e6799fc" + integrity sha512-Ese7052fdWrxp/vqSJkydgx/1MdBnNOCV2XVfbmdGWD2H6EYza+Q4pyYSuVSnCUD22hfI/BFI4jHaC3NLXLlJQ== + dependencies: + stream-buffers "2.2.x" + bplist-parser@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.1.1.tgz#d60d5dcc20cba6dc7e1f299b35d3e1f95dafbae6" @@ -1758,6 +1765,13 @@ bplist-parser@^0.1.0: dependencies: big-integer "^1.6.7" +bplist-parser@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.2.tgz#3ac79d67ec52c4c107893e0237eb787cbacbced7" + integrity sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ== + dependencies: + big-integer "1.6.x" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5236,6 +5250,11 @@ static-module@^2.2.0: static-eval "^2.0.0" through2 "~2.0.3" +stream-buffers@2.2.x: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" + integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg== + string-argv@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" From c532c72bc2e977c143d33620e9bcd5780c163023 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 1 Jun 2023 13:04:02 +0200 Subject: [PATCH 4/8] Fixes #83: Replace pkijs with forge --- docs/README.md | 8 +- package.json | 4 +- src/android.ts | 7 +- src/ios.ts | 65 +++++++------ src/types/forge.d.ts | 7 ++ src/utils/crypto.ts | 211 +++++++++++++------------------------------ yarn.lock | 49 +++------- 7 files changed, 126 insertions(+), 225 deletions(-) create mode 100644 src/types/forge.d.ts diff --git a/docs/README.md b/docs/README.md index 4b21e54..5e963c5 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:943](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L943) +[android.ts:946](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L946) ___ @@ -112,7 +112,7 @@ An ID of a known permission on iOS. #### Defined in -[ios.ts:565](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L565) +[ios.ts:570](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L570) ___ @@ -321,7 +321,7 @@ The IDs of known permissions on Android. #### Defined in -[android.ts:812](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L812) +[android.ts:815](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L815) ___ @@ -333,7 +333,7 @@ The IDs of known permissions on iOS. #### Defined in -[ios.ts:548](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L548) +[ios.ts:553](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L553) ## Functions diff --git a/package.json b/package.json index 7624276..43aa9b1 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@napi-rs/lzma": "^1.1.2", "andromatic": "^1.0.0", "autopy": "^1.1.1", - "asn1js": "^3.0.5", "bplist-creator": "^0.1.1", "bplist-parser": "^0.3.2", "cross-fetch": "^3.1.5", @@ -66,9 +65,9 @@ "fs-extra": "^11.1.0", "global-cache-dir": "^5.0.0", "ipa-extract-info": "^1.2.6", + "node-forge": "^1.3.1", "node-ssh": "^13.1.0", "p-retry": "^5.1.2", - "pkijs": "^3.0.14", "semver": "^7.3.8", "tempy": "^3.0.0", "ts-node": "^10.9.1", @@ -82,6 +81,7 @@ "@parcel/transformer-typescript-types": "2.8.2", "@types/fs-extra": "^11.0.0", "@types/node": "^18.11.18", + "@types/node-forge": "^1.3.2", "@types/plist": "^3.0.2", "@types/promise-timeout": "^1.3.0", "@types/semver": "^7.3.13", diff --git a/src/android.ts b/src/android.ts index 19a74e3..801fbe5 100644 --- a/src/android.ts +++ b/src/android.ts @@ -6,6 +6,7 @@ import { createHash, randomUUID } from 'crypto'; import { fileTypeFromFile } from 'file-type'; import frida from 'frida'; import { open, rm, writeFile } from 'fs/promises'; +import forge from 'node-forge'; import pRetry from 'p-retry'; import { basename, dirname } from 'path'; import { major as semverMajor, minVersion as semverMinVersion } from 'semver'; @@ -30,8 +31,8 @@ import { parseAppMeta, retryCondition, } from './utils'; +import { certSubjectToAsn1, parsePemCertificateFromFile } from './utils/crypto'; import { forEachInZip, getFileFromZip, tmpFileFromZipEntry } from './utils/zip'; -import { parsePemCertificateFromFile} from './utils/crypto'; const adb = (...args: ParametersExceptFirst) => runAndroidDevTool('adb', args[0], args[1]); const venv = getVenv(venvOptions); @@ -229,7 +230,9 @@ export const androidApi = >( getCertificateSubjectHashOld: async (path: string) => { const { cert } = await parsePemCertificateFromFile(path); - const hash = createHash('md5').update(Buffer.from(cert.subject.valueBeforeDecode)).digest(); + const hash = createHash('md5') + .update(forge.asn1.toDer(certSubjectToAsn1(cert)).toHex(), 'hex') + .digest(); const truncated = hash.subarray(0, 4); const ulong = (truncated[0]! | (truncated[1]! << 8) | (truncated[2]! << 16) | (truncated[3]! << 24)) >>> 0; diff --git a/src/ios.ts b/src/ios.ts index 52c5cd5..5d6799b 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -3,21 +3,24 @@ import bplist from 'bplist-creator'; import { parseFile } from 'bplist-parser'; import { createHash } from 'crypto'; import frida from 'frida'; -import { exists,mkdirp } from 'fs-extra'; -import { readFile,writeFile } from 'fs/promises'; +import { exists, mkdirp } from 'fs-extra'; +import { readFile, writeFile } from 'fs/promises'; import globalCacheDir from 'global-cache-dir'; import { NodeSSH } from 'node-ssh'; import { join } from 'path'; -import type { PlatformApi,PlatformApiOptions,Proxy,SupportedCapability,SupportedRunTarget } from '.'; +import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.'; import { venvOptions } from '../scripts/common/python'; -import { asyncUnimplemented,getObjFromFridaScript,isRecord,retryCondition } from './utils'; +import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils'; import { arrayBufferToPem, certificateFingerprint, certificateHasExpired, generateCertificate, parsePemCertificateFromFile, -pemToArrayBuffer +pemToArrayBuffer, +createPkcs12Container, +asn1ValueToDer, +certSubjectToAsn1, } from './utils/crypto'; const venv = getVenv(venvOptions); @@ -235,53 +238,54 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); if (!plist) throw new Error('Failed to ensure supervision mode: Invalid CloudConfiguration.'); let hostCert; - let hostKey; if ( (await exists(join(cacheDir, 'ios', 'supervisorCert.pem'))) && - (await exists(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))) + (await exists(join(cacheDir, 'ios', 'supervisorKeyStore.p12'))) ) { - hostCert = pemToArrayBuffer((await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString()); - hostKey = pemToArrayBuffer( - (await readFile(join(cacheDir, 'ios', 'supervisorPrivateKey.pem'))).toString() - ); + hostCert = (await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString(); if (!(await certificateHasExpired(hostCert))) { const hostCertFingerprint = await certificateFingerprint(hostCert); - // Test if the current host certificate is already controlling the device. - if ( - plist.IsSupervised && - plist.SupervisorHostCertificates && - plist.SupervisorHostCertificates.length > 0 && - plist.SupervisorHostCertificates.some( - async (cert) => (await certificateFingerprint(cert)) === hostCertFingerprint + try { + // Test if the current host certificate is already controlling the device. + if ( + plist.IsSupervised && + plist.SupervisorHostCertificates && + plist.SupervisorHostCertificates.length > 0 && + plist.SupervisorHostCertificates.some( + (cert) => + certificateFingerprint(arrayBufferToPem(cert, 'CERTIFICATE')) === + hostCertFingerprint + ) ) - ) - return; + return; + } catch (e) { + // The certificate is invalid, so we need to generate a new one. + hostCert = undefined; + } } else { hostCert = undefined; - hostKey = undefined; } } - if (!hostCert || !hostKey) { + if (!hostCert) { // We have no exsiting keys, so let’s generate one. const generated = await generateCertificate(OrganizationName); hostCert = generated.certificate; - hostKey = generated.privateKey; + const hostKey = generated.privateKey; + + const keyStore = createPkcs12Container(hostCert, hostKey, 'appstraction'); await mkdirp(join(cacheDir, 'ios')); - await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), arrayBufferToPem(hostCert, 'CERTIFICATE')); - await writeFile( - join(cacheDir, 'ios', 'supervisorPrivateKey.pem'), - arrayBufferToPem(hostKey, 'PRIVATE KEY') - ); + await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), hostCert); + await writeFile(join(cacheDir, 'ios', 'supervisorKeyStore.p12'), Buffer.from(keyStore.toHex(), 'hex')); } const newPlist = { ...plist, - SupervisorHostCertificates: [Buffer.from(hostCert)], + SupervisorHostCertificates: [Buffer.from(pemToArrayBuffer(hostCert))], IsSupervised: true, OrganizationName, AllowPairing: true, @@ -488,7 +492,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); const { cert, certDer } = await parsePemCertificateFromFile(path); const sha256 = createHash('sha256').update(certDer).digest('hex'); - const subj = Buffer.from(cert.subject.toSchema().valueBlock.toBER()).toString('hex'); + const subj = asn1ValueToDer(certSubjectToAsn1(cert)).toHex(); const tset = Buffer.from( ` @@ -507,6 +511,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); throw new Error('SSH is required for removing a certificate authority.'); const { certDer } = await parsePemCertificateFromFile(path); + const sha256 = createHash('sha256').update(certDer).digest('hex'); await this._internal.ssh( diff --git a/src/types/forge.d.ts b/src/types/forge.d.ts new file mode 100644 index 0000000..990c4b7 --- /dev/null +++ b/src/types/forge.d.ts @@ -0,0 +1,7 @@ +import type forge from 'node-forge'; + +declare module 'node-forge' { + namespace pki { + function distinguishedNameToAsn1(dn: forge.pki.Certificate['subject' | 'issuer']): forge.pki.Asn1; + } +} diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index ed79a39..8c02d93 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -1,161 +1,65 @@ -import { BmpString, Integer } from 'asn1js'; -import { webcrypto } from 'crypto'; import { readFile } from 'fs/promises'; -import { - AttributeTypeAndValue, - AuthenticatedSafe, - CertBag, - Certificate, - CryptoEngine, - PFX, - PKCS8ShroudedKeyBag, - PrivateKeyInfo, - SafeBag, - SafeContents, - setEngine, -} from 'pkijs'; - -const crypto = new CryptoEngine({ name: 'node-webcrypto', crypto: webcrypto as Crypto }); -setEngine('node-webcrypto', crypto); // We need to do this, because there is a bug in pkijs (https://github.com/PeculiarVentures/PKI.js/issues/379) +import forge from 'node-forge'; +const { pki, md } = forge; export const generateCertificate = async (commonName: string, days?: number) => { - const algorithm = crypto.getAlgorithmParameters('RSA-PSS', 'generateKey'); - const { privateKey, publicKey } = await crypto.generateKey( - algorithm.algorithm as EcKeyAlgorithm, - true, - algorithm.usages - ); + const keyPair = await new Promise((res, rej) => { + pki.rsa.generateKeyPair({ bits: 2048 }, (err, keyPair) => (err ? rej(err) : res(keyPair))); + }); + const cert = pki.createCertificate(); - const cert = new Certificate(); + cert.publicKey = keyPair.publicKey; cert.version = 2; - cert.serialNumber = new Integer({ value: Date.now() }); - cert.notBefore.value = new Date(); - cert.notAfter.value = new Date(); - cert.notAfter.value.setDate(cert.notBefore.value.getDate() + (days || 365)); + cert.serialNumber = Date.now().toString(10); + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + (days || 365)); - cert.issuer.typesAndValues.push( - new AttributeTypeAndValue({ - type: '2.5.4.3', // Common name - value: new BmpString({ value: commonName }), - }) - ); - cert.subject.typesAndValues.push( - new AttributeTypeAndValue({ - type: '2.5.4.3', // Common name - value: new BmpString({ value: commonName }), - }) - ); + const attributes = [ + { + name: 'commonName', + value: commonName, + }, + ]; + cert.setSubject(attributes); + cert.setIssuer(attributes); - await cert.subjectPublicKeyInfo.importKey(publicKey, crypto); - await cert.sign(privateKey, 'SHA-256', crypto); + cert.sign(keyPair.privateKey, md.sha256.create()); return { - certificate: cert.toSchema().toBER(false), - privateKey: await crypto.exportKey('pkcs8', privateKey), + certificate: pki.certificateToPem(cert), + privateKey: pki.privateKeyToPem(keyPair.privateKey), }; }; -export const certificateFingerprint = async (certificateBuffer: ArrayBuffer, hashAlgorithm?: 'SHA-256' | 'SHA-1') => { - const certificate = await Certificate.fromBER(certificateBuffer); - const hash = await crypto.digest( - hashAlgorithm || 'SHA-256', - certificate.subjectPublicKeyInfo.toSchema().toBER(false) - ); - return Buffer.from(hash).toString('hex'); +export const certificateFingerprint = (certificatePem: string, hashAlgorithm?: 'SHA-256' | 'SHA-1') => { + const cert = pki.certificateFromPem(certificatePem); + return pki.getPublicKeyFingerprint(cert.publicKey, { + type: 'SubjectPublicKeyInfo', + md: hashAlgorithm === 'SHA-1' ? md.sha1.create() : md.sha256.create(), + encoding: 'hex', + }); }; -export const certificateHasExpired = async (certificateBuffer: ArrayBuffer) => { - const certificate = await Certificate.fromBER(certificateBuffer); - return certificate.notAfter.value < new Date(); +export const certificateHasExpired = (certificatePem: string) => { + const cert = pki.certificateFromPem(certificatePem); + return cert.validity.notAfter < new Date(); }; -export const createPkcs12Container = async (cert: ArrayBuffer, key: ArrayBuffer, password?: string) => { - const encodedPassword = new TextEncoder().encode(password || '').buffer; - - const pkcs12 = new PFX({ - parsedValue: { - integrityMode: 0, // Password-Based Integrity Mode - authenticatedSafe: new AuthenticatedSafe({ - parsedValue: { - safeContents: [ - { - privacyMode: 0, // 0 - No privacy mode - value: new SafeContents({ - safeBags: [ - new SafeBag({ - bagId: '1.2.840.113549.1.12.10.1.2', // Shrouded Private Key Bag - bagValue: new PKCS8ShroudedKeyBag({ - parsedValue: PrivateKeyInfo.fromBER(key), - }), - }), - ], - }), - }, - { - privacyMode: 1, // 1 - Password based privacy mode, - value: new SafeContents({ - safeBags: [ - new SafeBag({ - bagId: '1.2.840.113549.1.12.10.1.3', // Certificate bag - bagValue: new CertBag({ - parsedValue: Certificate.fromBER(cert), - }), - }), - ], - }), - }, - ], - }, - }), - }, - }); - - if (!pkcs12.parsedValue?.authenticatedSafe) - throw new Error('Broken certificate container: pkcs12.parsedValue.authenticatedSafe is empty'); - - await pkcs12.parsedValue.authenticatedSafe.parsedValue.safeContents[0].value.safeBags[0].bagValue.makeInternalValues( - { - password: encodedPassword, - contentEncryptionAlgorithm: { - name: 'AES-CBC', // OpenSSL can only handle AES-CBC (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L438) - length: 128, - }, - hmacHashAlgorithm: 'SHA-1', // OpenSSL can only handle SHA-1 (https://github.com/PeculiarVentures/PKI.js/blob/469c403d102ee5149e8eb9ad19754c9696ed7c55/test/pkcs12SimpleExample.ts#L441) - iterationCount: 100000, - }, - crypto - ); - - pkcs12.parsedValue.authenticatedSafe.makeInternalValues( - { - safeContents: [ - { - // Private key contents are encrypted differently, so this needs to be empty. - }, - { - password: encodedPassword, - contentEncryptionAlgorithm: { - name: 'AES-CBC', - length: 128, - }, - hmacHashAlgorithm: 'SHA-1', - iterationCount: 100000, - }, - ], - }, - crypto +export const createPkcs12Container = ( + certPem: string, + keyPem: string, + password?: string, + algorithm?: 'aes256' | '3des' +) => { + const p12 = forge.pkcs12.toPkcs12Asn1( + pki.privateKeyFromPem(keyPem), + pki.certificateFromPem(certPem), + password || '', + { algorithm: algorithm || '3des' } // Apparently any sane algorithm is not supported by the typical ingestors (like go), so we default to 3des ); - await pkcs12.makeInternalValues( - { - password: encodedPassword, - iterations: 100000, - pbkdf2HashAlgorithm: 'SHA-256', - hmacHashAlgorithm: 'SHA-256', - }, - crypto - ); - return pkcs12.toSchema().toBER(); + return forge.asn1.toDer(p12); }; export const arrayBufferToPem = (buffer: ArrayBuffer, tag: 'CERTIFICATE' | 'PRIVATE KEY' | 'PUBLIC KEY') => { @@ -163,16 +67,6 @@ export const arrayBufferToPem = (buffer: ArrayBuffer, tag: 'CERTIFICATE' | 'PRIV return `-----BEGIN ${tag}-----\n${base64.replace(/(.{64})/g, '$1\n').trim()}\n-----END ${tag}-----`; // Thanks Copilot! }; -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 }; -}; - export const pemToArrayBuffer = (pem: string) => { const base64 = pem .replace(/-----BEGIN (.*)-----/, '') @@ -180,3 +74,20 @@ export const pemToArrayBuffer = (pem: string) => { .replace(/\n/g, ''); return Uint8Array.from(Buffer.from(base64, 'base64')).buffer; }; + +export const parsePemCertificateFromFile = async (path: string) => { + const certPem = await readFile(path, 'utf8'); + const cert = pki.certificateFromPem(certPem); + + return { cert, certPem, certDer: Buffer.from(pemToArrayBuffer(certPem)) }; +}; + +export const certSubjectToAsn1 = (cert: forge.pki.Certificate) => forge.pki.distinguishedNameToAsn1(cert.subject); + +export const asn1ValueToDer = (asn1: forge.asn1.Asn1) => + typeof asn1.value === 'string' + ? forge.util.createBuffer(asn1.value) + : asn1.value.reduce((acc, cur) => { + acc.putBuffer(forge.asn1.toDer(cur)); + return acc; + }, forge.util.createBuffer()); diff --git a/yarn.lock b/yarn.lock index 33df9b3..d96f739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1337,6 +1337,13 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-forge@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.2.tgz#4aee4f269961fe288fe6dc35ad0ec6f71646d2bc" + integrity sha512-TzX3ahoi9xbmaoT58smrBu7oa6dQXb/+PTNCslZyD/55tlJ/osofIMClzZsoo6buDFrg7e4DvVGkZqVgv6OLxw== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@^18.11.18": version "18.11.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" @@ -1639,15 +1646,6 @@ asn1@^0.2.4: dependencies: safer-buffer "~2.1.0" -asn1js@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" - integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== - dependencies: - pvtsutils "^1.3.2" - pvutils "^1.1.3" - tslib "^2.4.0" - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -1881,11 +1879,6 @@ buildcheck@0.0.5: resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.5.tgz#5b7c0830b25dc61422032eeb5c18bfcaa9eebb8d" integrity sha512-jYWpRy8eedl/JZqkOeq0X0bNcaK04hXKhIi4gYsDKZUJWRjJJWViYfsMXO0BJQ40zSLcdLoa+iqe48Kz2PtQag== -bytestreamjs@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" - integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== - cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -4293,6 +4286,11 @@ node-fetch@^2.6.6, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + node-gyp-build-optional-packages@5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" @@ -4629,17 +4627,6 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -pkijs@^3.0.14: - version "3.0.14" - resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.0.14.tgz#92571f61122fd1e0ccdbc328b6efab9467b0bc30" - integrity sha512-Fi9++44BaOY0VcOEJql27D/HzHIeMU9R48XclfL98Cp8Wh/gGfPbuS1RUwReHQHRIUfzW32eoNO1izxoBMZi6w== - dependencies: - asn1js "^3.0.5" - bytestreamjs "^2.0.0" - pvtsutils "^1.3.2" - pvutils "^1.1.3" - tslib "^2.4.0" - please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -4752,18 +4739,6 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pvtsutils@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de" - integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ== - dependencies: - tslib "^2.4.0" - -pvutils@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" - integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" From dbbf2fb9e656f9e3de8675e0a0e19bd35fac12d2 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 1 Jun 2023 13:22:20 +0200 Subject: [PATCH 5/8] Add `supervision` capability --- docs/README.md | 26 ++++++++++++------------- src/index.ts | 30 +++++++++++++++++++--------- src/ios.ts | 53 ++++++++++++++++++++++++++++++++------------------ 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/docs/README.md b/docs/README.md index 5e963c5..f9844e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -81,7 +81,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the #### Defined in -[index.ts:424](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L424) +[index.ts:436](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L436) ___ @@ -100,7 +100,7 @@ The options for each attribute available through the `getDeviceAttribute()` func #### Defined in -[index.ts:430](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L430) +[index.ts:442](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L442) ___ @@ -112,7 +112,7 @@ An ID of a known permission on iOS. #### Defined in -[ios.ts:570](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L570) +[ios.ts:585](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L585) ___ @@ -200,7 +200,7 @@ The options for the `platformApi()` function. #### Defined in -[index.ts:362](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L362) +[index.ts:365](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L365) ___ @@ -219,7 +219,7 @@ Connection details for a proxy. #### Defined in -[index.ts:438](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L438) +[index.ts:450](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L450) ___ @@ -243,19 +243,19 @@ The options for a specific platform/run target combination. | `android` | { `device`: `unknown` ; `emulator`: `unknown` } | The options for the Android platform. | | `android.device` | `unknown` | The options for the Android physical device run target. | | `android.emulator` | `unknown` | The options for the Android emulator run target. | -| `ios` | { `device`: ``"ssh"`` extends `Capability` ? { `ip`: `string` ; `rootPw?`: `string` } : `unknown` ; `emulator`: `never` } | The options for the iOS platform. | -| `ios.device` | ``"ssh"`` extends `Capability` ? { `ip`: `string` ; `rootPw?`: `string` } : `unknown` | The options for the iOS physical device run target. | +| `ios` | { `device`: ``"ssh"`` extends `Capability` ? { `ip`: `string` ; `rootPw?`: `string` } : `unknown` & ``"supervision"`` extends `Capability` ? { `supervisionKeyPassword?`: `string` } : `unknown` ; `emulator`: `never` } | The options for the iOS platform. | +| `ios.device` | ``"ssh"`` extends `Capability` ? { `ip`: `string` ; `rootPw?`: `string` } : `unknown` & ``"supervision"`` extends `Capability` ? { `supervisionKeyPassword?`: `string` } : `unknown` | The options for the iOS physical device run target. | | `ios.emulator` | `never` | The options for the iOS emulator run target. | #### Defined in -[index.ts:389](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L389) +[index.ts:392](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L392) ___ ### SupportedCapability -Ƭ **SupportedCapability**<`Platform`\>: `Platform` extends ``"android"`` ? ``"wireguard"`` \| ``"root"`` \| ``"frida"`` \| ``"certificate-pinning-bypass"`` : `Platform` extends ``"ios"`` ? ``"ssh"`` \| ``"frida"`` \| ``"certificate-pinning-bypass"`` : `never` +Ƭ **SupportedCapability**<`Platform`\>: `Platform` extends ``"android"`` ? ``"wireguard"`` \| ``"root"`` \| ``"frida"`` \| ``"certificate-pinning-bypass"`` : `Platform` extends ``"ios"`` ? ``"ssh"`` \| ``"frida"`` \| ``"certificate-pinning-bypass"`` \| ``"supervision"`` : `never` A capability for the `platformApi()` function. @@ -267,7 +267,7 @@ A capability for the `platformApi()` function. #### Defined in -[index.ts:417](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L417) +[index.ts:429](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L429) ___ @@ -309,7 +309,7 @@ Configuration string for WireGuard. #### Defined in -[index.ts:445](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L445) +[index.ts:457](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L457) ## Variables @@ -333,7 +333,7 @@ The IDs of known permissions on iOS. #### Defined in -[ios.ts:553](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L553) +[ios.ts:568](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L568) ## Functions @@ -426,4 +426,4 @@ The API object for the given platform and run target. #### Defined in -[index.ts:454](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L454) +[index.ts:466](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L466) diff --git a/src/index.ts b/src/index.ts index b19b543..1a69427 100644 --- a/src/index.ts +++ b/src/index.ts @@ -329,21 +329,24 @@ export type PlatformApi< /** * Ensures that the current host is configured to supervise the connected device. If this is not the case, * it sets the host to be the (only) supervisor by installing its certificate on the device. This will - * overwrite parts of the exisiting CloudConfiguration. If there is no host certificate, yet, or it has + * overwrite parts of the exisiting CloudConfiguration. If there is no host certificate yet, or it has * expired, it will be generated. * - * Might restart the device, if a new cofniguration is pushed. You are adviced to wait for the device. + * Might restart the device, if a new configuration is pushed. You are advised to wait for the device. + * + * @param forceNewKey If set to `true`, a new host key will be generated and set up, even if there is + * already a valid old one. */ - ensureSupervision: () => Promise; + ensureSupervision: (options?: { forceNewKey?: boolean }) => Promise; /** * Removes all configured supervision hosts from the device. * - * Will restart the device. You are adviced to wait for the device. + * Will restart the device. You are advised to wait for the device. */ removeSupervision: () => Promise; /** - * Restarts the device only in the userspace, e.g. to keep the jailbroken kernel running. You might want - * to wait for the device to ensure it is available again. + * Restarts the device only in userspace, e.g. to keep the jailbroken kernel running. You might want to + * wait for the device to ensure it is available again. */ userspaceRestart: () => Promise; } @@ -402,14 +405,23 @@ export type RunTargetOptions< /** The options for the iOS emulator run target. */ emulator: never; /** The options for the iOS physical device run target. */ - device: 'ssh' extends Capability + device: ('ssh' extends Capability ? { /** The password of the root user on the device, defaults to `alpine` if not set. */ rootPw?: string; /** The device's IP address. */ ip: string; } - : unknown; + : unknown) & + ('supervision' extends Capability + ? { + /** + * The password of the private key of the supervision certificate, defaults to `appstraction` if + * not set. + */ + supervisionKeyPassword?: string; + } + : unknown); }; }; @@ -417,7 +429,7 @@ export type RunTargetOptions< export type SupportedCapability = Platform extends 'android' ? 'wireguard' | 'root' | 'frida' | 'certificate-pinning-bypass' : Platform extends 'ios' - ? 'ssh' | 'frida' | 'certificate-pinning-bypass' + ? 'ssh' | 'frida' | 'certificate-pinning-bypass' | 'supervision' : never; /** A supported attribute for the `getDeviceAttribute()` function, depending on the platform. */ diff --git a/src/ios.ts b/src/ios.ts index 5d6799b..806f526 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -12,15 +12,15 @@ import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, Suppo import { venvOptions } from '../scripts/common/python'; import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils'; import { -arrayBufferToPem, -certificateFingerprint, -certificateHasExpired, -generateCertificate, -parsePemCertificateFromFile, -pemToArrayBuffer, -createPkcs12Container, -asn1ValueToDer, -certSubjectToAsn1, + arrayBufferToPem, + asn1ValueToDer, + certificateFingerprint, + certificateHasExpired, + certSubjectToAsn1, + createPkcs12Container, + generateCertificate, + parsePemCertificateFromFile, + pemToArrayBuffer, } from './utils/crypto'; const venv = getVenv(venvOptions); @@ -223,11 +223,11 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); }); await session.detach(); }, - async ensureSupervision() { + async ensureSupervision(supervisionOptions) { if (!options.capabilities.includes('ssh')) - throw new Error('SSH is currently required to ensure supervison mode.'); + throw new Error('SSH is currently required to ensure supervision mode.'); - const OrganizationName = 'appstraction'; + const orgName = 'appstraction'; const cacheDir = await globalCacheDir('appstraction'); const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`); @@ -240,13 +240,14 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); let hostCert; if ( + !supervisionOptions?.forceNewKey && (await exists(join(cacheDir, 'ios', 'supervisorCert.pem'))) && (await exists(join(cacheDir, 'ios', 'supervisorKeyStore.p12'))) ) { hostCert = (await readFile(join(cacheDir, 'ios', 'supervisorCert.pem'))).toString(); - if (!(await certificateHasExpired(hostCert))) { - const hostCertFingerprint = await certificateFingerprint(hostCert); + if (!certificateHasExpired(hostCert)) { + const hostCertFingerprint = certificateFingerprint(hostCert); try { // Test if the current host certificate is already controlling the device. @@ -270,13 +271,17 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); } } - if (!hostCert) { - // We have no exsiting keys, so let’s generate one. - const generated = await generateCertificate(OrganizationName); + if (!hostCert || supervisionOptions?.forceNewKey) { + // We have no existing keys, so let’s generate one. + const generated = await generateCertificate(orgName); hostCert = generated.certificate; const hostKey = generated.privateKey; - const keyStore = createPkcs12Container(hostCert, hostKey, 'appstraction'); + const keyStore = createPkcs12Container( + hostCert, + hostKey, + options.targetOptions?.supervisionKeyPassword || 'appstraction' + ); await mkdirp(join(cacheDir, 'ios')); await writeFile(join(cacheDir, 'ios', 'supervisorCert.pem'), hostCert); @@ -287,7 +292,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); ...plist, SupervisorHostCertificates: [Buffer.from(pemToArrayBuffer(hostCert))], IsSupervised: true, - OrganizationName, + OrganizationName: orgName, AllowPairing: true, }; @@ -357,6 +362,16 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); if (options.capabilities.includes('frida')) { await this._internal.ensureFrida(); } + + if (options.capabilities.includes('supervision')) { + if (!options.capabilities.includes('ssh')) + throw new Error( + 'Unimplemented on this platform: Activating supervision mode without the ssh capability.' + ); + + await this._internal.ensureSupervision(); + await this.waitForDevice(); + } }, clearStuckModals: asyncUnimplemented('clearStuckModals') as never, From ca16939ca4aeab75dcbe5fa8bcaa8e7cf279d30b Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 1 Jun 2023 18:31:06 +0200 Subject: [PATCH 6/8] Implement changes from testing on iOS 16 --- package.json | 2 +- src/ios.ts | 8 ++++---- yarn.lock | 32 ++++++++++++++++++++++++-------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 43aa9b1..fb1c431 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "andromatic": "^1.0.0", "autopy": "^1.1.1", "bplist-creator": "^0.1.1", - "bplist-parser": "^0.3.2", "cross-fetch": "^3.1.5", "execa": "^6.1.0", "file-type": "^18.3.0", @@ -69,6 +68,7 @@ "node-ssh": "^13.1.0", "p-retry": "^5.1.2", "semver": "^7.3.8", + "simple-plist": "^1.3.1", "tempy": "^3.0.0", "ts-node": "^10.9.1", "yauzl": "^2.10.0" diff --git a/src/ios.ts b/src/ios.ts index 806f526..e7c9418 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -1,6 +1,5 @@ import { getVenv } from 'autopy'; import bplist from 'bplist-creator'; -import { parseFile } from 'bplist-parser'; import { createHash } from 'crypto'; import frida from 'frida'; import { exists, mkdirp } from 'fs-extra'; @@ -8,6 +7,7 @@ import { readFile, writeFile } from 'fs/promises'; import globalCacheDir from 'global-cache-dir'; import { NodeSSH } from 'node-ssh'; import { join } from 'path'; +import simplePlist from 'simple-plist'; import type { PlatformApi, PlatformApiOptions, Proxy, SupportedCapability, SupportedRunTarget } from '.'; import { venvOptions } from '../scripts/common/python'; import { asyncUnimplemented, getObjFromFridaScript, isRecord, retryCondition } from './utils'; @@ -231,7 +231,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); const cacheDir = await globalCacheDir('appstraction'); const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`); - const plist = (await parseFile(Buffer.from(encodedPlist, 'base64')))?.[0] as + const plist = (await simplePlist.parse(Buffer.from(encodedPlist, 'base64'))) as | CloudConfigurationDetails | undefined; @@ -301,7 +301,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); }, async removeSupervision() { const { stdout: encodedPlist } = await this.ssh(`cat ${cloudConfigPath} | base64`); - const plist = (await parseFile(Buffer.from(encodedPlist, 'base64')))?.[0] as + const plist = (await simplePlist.parse(Buffer.from(encodedPlist, 'base64'))) as | CloudConfigurationDetails | undefined; @@ -320,7 +320,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); if (!options.capabilities.includes('ssh')) throw new Error('SSH is currently required to restart in userspace.'); - await this.ssh('ldrestart'); + await this.ssh('launchctl reboot userspace'); }, }, diff --git a/yarn.lock b/yarn.lock index d96f739..d8957dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1749,6 +1749,13 @@ bottleneck@^2.15.3: resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== +bplist-creator@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e" + integrity sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg== + dependencies: + stream-buffers "2.2.x" + bplist-creator@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.1.tgz#ef638af058a7021e10ebfd557ffd73d95e6799fc" @@ -1756,6 +1763,13 @@ bplist-creator@^0.1.1: dependencies: stream-buffers "2.2.x" +bplist-parser@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.1.tgz#e1c90b2ca2a9f9474cc72f6862bbf3fee8341fd1" + integrity sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA== + dependencies: + big-integer "1.6.x" + bplist-parser@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.1.1.tgz#d60d5dcc20cba6dc7e1f299b35d3e1f95dafbae6" @@ -1763,13 +1777,6 @@ bplist-parser@^0.1.0: dependencies: big-integer "^1.6.7" -bplist-parser@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.2.tgz#3ac79d67ec52c4c107893e0237eb787cbacbced7" - integrity sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ== - dependencies: - big-integer "1.6.x" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -4634,7 +4641,7 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" -plist@^3.0.4: +plist@^3.0.4, plist@^3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA== @@ -5104,6 +5111,15 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" +simple-plist@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.3.1.tgz#16e1d8f62c6c9b691b8383127663d834112fb017" + integrity sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw== + dependencies: + bplist-creator "0.1.0" + bplist-parser "0.3.1" + plist "^3.0.5" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" From c5b854c9d5e5558b4528f9128d06c11c60b30ec6 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Mon, 26 Jun 2023 21:23:53 +0200 Subject: [PATCH 7/8] Fixes #107: Simulate buttons to unlock the devices --- docs/README.md | 25 +++++++++++++------------ src/android.ts | 9 +++++++++ src/index.ts | 3 +++ src/ios.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index f9844e0..8d26da0 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:946](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L946) +[android.ts:955](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L955) ___ @@ -81,7 +81,7 @@ A supported attribute for the `getDeviceAttribute()` function, depending on the #### Defined in -[index.ts:436](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L436) +[index.ts:439](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L439) ___ @@ -100,7 +100,7 @@ The options for each attribute available through the `getDeviceAttribute()` func #### Defined in -[index.ts:442](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L442) +[index.ts:445](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L445) ___ @@ -112,7 +112,7 @@ An ID of a known permission on iOS. #### Defined in -[ios.ts:585](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L585) +[ios.ts:627](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L627) ___ @@ -176,6 +176,7 @@ Functions that are available for the platforms. | `target.platform` | `Platform` | The platform this instance is configured for, i.e. `ios` or `android`. | | `target.runTarget` | `RunTarget` | The run target this instance is configured for, i.e. `device` or `emulator`. | | `uninstallApp` | (`appId`: `string`) => `Promise`<`void`\> | Uninstall the app with the given app ID. Will not fail if the app is not installed. This also removes any data stored by the app. | +| `unlockScreen` | () => `Promise`<`void`\> | Simulates key presses to unlock the screen. This only works if no passcode is set on the device. | | `waitForDevice` | (`tries?`: `number`) => `Promise`<`void`\> | Wait until the device or emulator has been connected and has booted up completely. | #### Defined in @@ -200,7 +201,7 @@ The options for the `platformApi()` function. #### Defined in -[index.ts:365](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L365) +[index.ts:368](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L368) ___ @@ -219,7 +220,7 @@ Connection details for a proxy. #### Defined in -[index.ts:450](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L450) +[index.ts:453](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L453) ___ @@ -249,7 +250,7 @@ The options for a specific platform/run target combination. #### Defined in -[index.ts:392](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L392) +[index.ts:395](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L395) ___ @@ -267,7 +268,7 @@ A capability for the `platformApi()` function. #### Defined in -[index.ts:429](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L429) +[index.ts:432](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L432) ___ @@ -309,7 +310,7 @@ Configuration string for WireGuard. #### Defined in -[index.ts:457](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L457) +[index.ts:460](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L460) ## Variables @@ -321,7 +322,7 @@ The IDs of known permissions on Android. #### Defined in -[android.ts:815](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L815) +[android.ts:824](https://github.com/tweaselORG/appstraction/blob/main/src/android.ts#L824) ___ @@ -333,7 +334,7 @@ The IDs of known permissions on iOS. #### Defined in -[ios.ts:568](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L568) +[ios.ts:610](https://github.com/tweaselORG/appstraction/blob/main/src/ios.ts#L610) ## Functions @@ -426,4 +427,4 @@ The API object for the given platform and run target. #### Defined in -[index.ts:466](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L466) +[index.ts:469](https://github.com/tweaselORG/appstraction/blob/main/src/index.ts#L469) diff --git a/src/android.ts b/src/android.ts index 801fbe5..00a1f1d 100644 --- a/src/android.ts +++ b/src/android.ts @@ -809,6 +809,15 @@ export const androidApi = >( ) throw new Error('Failed to set proxy.'); }, + unlockScreen: async () => { + const { stdout } = await adb(['shell', 'dumpsys', 'window']); + if (stdout.includes('mAwake=false')) + // The screen is off, simulate lock button press + await adb(['shell', 'input', 'keyevent', '26']); + if (stdout.includes('mShowingLockscreen=true') || stdout.includes('mDreamingLockscreen=true')) + // The screen is locked, simulate a menu key press + await adb(['shell', 'input', 'keyevent', '82']); + }, }); /** The IDs of known permissions on Android. */ diff --git a/src/index.ts b/src/index.ts index 1a69427..9a719b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -285,6 +285,9 @@ export type PlatformApi< : Platform extends 'ios' ? (proxy: Proxy | null) => Promise : never; + + /** Simulates key presses to unlock the screen. This only works if no passcode is set on the device. */ + unlockScreen: () => Promise; /** * An indicator for what platform and run target this instance of PlatformApi is configured for. This is useful * mostly to write typeguards. diff --git a/src/ios.ts b/src/ios.ts index e7c9418..70f4d40 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -135,6 +135,13 @@ function getProxySettingsForCurrentWifiNetwork() { } send({ name: "get_obj_from_frida_script", payload: getProxySettingsForCurrentWifiNetwork() });`, + simulateHomeButton: `// See https://github.com/tweaselORG/appstraction/issues/107 +var atServer = ObjC.classes.HNDAssistiveTouchServer.sharedInstance(); +// frida somehow needs this to attach the method to the object (https://github.com/tweaselORG/appstraction/issues/107#issuecomment-1608013662) +Object.getOwnPropertyNames(atServer) +atServer._home() +// The process will always crash after this, but the home button press will be simulated before that. +`, } as const; const cloudConfigPath = '/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/CloudConfigurationDetails.plist' as const; @@ -555,6 +562,40 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); ) throw new Error('Failed to set proxy.'); }, + unlockScreen: async () => { + if (!options.capabilities.includes('frida')) throw new Error('Frida is required for unlocking the screen.'); + await frida + .getUsbDevice() + .then((f) => f.attach('assistivetouchd')) + .then(async (s) => { + await (await s.createScript(fridaScripts.simulateHomeButton)).load(); + await s.detach(); + }) + .catch((e) => { + if (e.message === 'Process not found') + throw new Error( + 'AssistiveTouch service is not running. Enable it in Settings > Accessibility > Touch > AssistiveTouch.' + ); + // TODO: Enable AssistiveTouch automatically. This can be done via lockdownd, but is not supported by pymobiledevice3, yet. + }); + // Since assistivetouchd always crashes after the simulated home button press, we need to wait for it to restart. + await retryCondition( + () => + python('pymobiledevice3', ['processes', 'ps', '--no-color']).then(({ stdout }) => + Object.values(JSON.parse(stdout) as Record>).some( + (p) => p['ProcessName'] === 'assistivetouchd' + ) + ), + 5 + ); + await frida + .getUsbDevice() + .then((f) => f.attach('assistivetouchd')) + .then(async (s) => { + await (await s.createScript(fridaScripts.simulateHomeButton)).load(); + await s.detach(); + }); + }, }); type CloudConfigurationDetails = Partial<{ From f9690dfb159708b54768ab750ac417ac9a9dd73e Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Mon, 26 Jun 2023 23:16:39 +0200 Subject: [PATCH 8/8] Leave the device in a ready state after supervision --- src/ios.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ios.ts b/src/ios.ts index 70f4d40..1b1dc94 100644 --- a/src/ios.ts +++ b/src/ios.ts @@ -161,7 +161,7 @@ export const iosApi = >( // Creating and disposing a new SSH connection for each command is not efficient but it replicates the // previous behaviour of calling `ssh`. If we wanted to keep the connection open, we would also need a way // to dispose of it at the very end, but we don't know when that is (cf. #24). - ssh.dispose(); + if (ssh.connection) ssh.dispose(); return res; }, async setupEnvironment() { @@ -378,6 +378,7 @@ Components:" > /etc/apt/sources.list.d/appstraction.sources`); await this._internal.ensureSupervision(); await this.waitForDevice(); + await this.unlockScreen(); } }, clearStuckModals: asyncUnimplemented('clearStuckModals') as never,